/**
 * Instagram extractor — yt-dlp subprocess implementation.
 *
 * Replaces the Playwright-based scraper. yt-dlp is maintained against
 * Instagram's frontend churn, has no in-process state, and works on public
 * reels without authentication. Login-walled reels can be supported by
 * dropping a Netscape-format cookies file at the path under SECRETS_DIR.
 */

import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { existsSync, readFileSync, writeFileSync, statSync } from 'node:fs';
import { logError } from './utils/logger';
import type { ExtractedContent, ProgressCallback } from './extraction';

const execFileAsync = promisify(execFile);

const YTDLP_TIMEOUT_MS = 60_000;
const IMAGE_FETCH_TIMEOUT_MS = 10_000;
const USER_AGENT =
	'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1';

const AUTH_PATHS = ['/app/secrets/auth.json', './secrets/auth.json'];
const COOKIE_PATHS = ['/app/secrets/cookies.txt', './secrets/cookies.txt'];

interface PlaywrightCookie {
	name: string;
	value: string;
	domain: string;
	path: string;
	expires: number;
	httpOnly?: boolean;
	secure?: boolean;
}

/** Convert Playwright auth.json → Netscape cookies.txt next to it. */
function maybeConvertAuthJson(): void {
	for (let i = 0; i < AUTH_PATHS.length; i++) {
		const authPath = AUTH_PATHS[i];
		const cookiePath = COOKIE_PATHS[i];
		if (!existsSync(authPath)) continue;

		// Re-generate whenever auth.json is newer than the existing cookies.txt
		const authMtime = statSync(authPath).mtimeMs;
		const cookieMtime = existsSync(cookiePath) ? statSync(cookiePath).mtimeMs : 0;
		if (cookieMtime >= authMtime) continue;

		try {
			const auth = JSON.parse(readFileSync(authPath, 'utf8')) as {
				cookies?: PlaywrightCookie[];
			};
			const cookies: PlaywrightCookie[] = auth.cookies ?? [];
			const lines = [
				'# Netscape HTTP Cookie File',
				'# Auto-generated from auth.json by InstaChef.',
				''
			];
			for (const c of cookies) {
				const domain = c.domain.startsWith('.') ? c.domain : `.${c.domain}`;
				const includeSubdomains = 'TRUE';
				const secure = c.secure ? 'TRUE' : 'FALSE';
				const expiry = Math.floor(c.expires > 0 ? c.expires : 0);
				lines.push(
					[domain, includeSubdomains, c.path ?? '/', secure, expiry, c.name, c.value].join('\t')
				);
			}
			writeFileSync(cookiePath, lines.join('\n') + '\n', 'utf8');
		} catch {
			// Non-fatal: yt-dlp will just run without cookies
		}
		break;
	}
}

function resolveCookiePath(): string | null {
	maybeConvertAuthJson();
	for (const p of COOKIE_PATHS) {
		if (existsSync(p)) return p;
	}
	return null;
}

interface YtDlpJson {
	description?: string | null;
	title?: string | null;
	thumbnail?: string | null;
	thumbnails?: Array<{ url?: string }>;
}

function pickThumbnailUrl(data: YtDlpJson): string | null {
	if (data.thumbnail) return data.thumbnail;
	const first = (data.thumbnails ?? []).find((t) => t?.url);
	return first?.url ?? null;
}

async function fetchImageAsBase64(imageUrl: string): Promise<string | null> {
	try {
		const response = await fetch(imageUrl, {
			signal: AbortSignal.timeout(IMAGE_FETCH_TIMEOUT_MS)
		});
		if (response.status !== 200) return null;
		const contentType = response.headers.get('content-type') ?? '';
		if (!contentType.startsWith('image/')) return null;
		const buf = Buffer.from(await response.arrayBuffer());
		return `data:${contentType};base64,${buf.toString('base64')}`;
	} catch (e) {
		logError('[ytdlp] Thumbnail fetch failed', e);
		return null;
	}
}

function classifyYtDlpError(stderr: string): { recoverable: boolean; reason: string } {
	const lower = stderr.toLowerCase();
	if (
		lower.includes('login required') ||
		lower.includes('login_required') ||
		lower.includes('private') ||
		lower.includes('rate-limit') ||
		lower.includes('rate limit')
	) {
		return {
			recoverable: false,
			reason:
				'Instagram requires authentication for this reel. Drop a Netscape cookies.txt at secrets/cookies.txt and retry.'
		};
	}
	if (lower.includes('unsupported url')) {
		return { recoverable: false, reason: 'URL not recognised by yt-dlp.' };
	}
	if (lower.includes('http error 404') || lower.includes('does not exist')) {
		return { recoverable: false, reason: 'Reel not found (404).' };
	}
	return { recoverable: true, reason: stderr.split('\n').filter(Boolean).slice(-2).join(' ') };
}

/**
 * Extract caption text + thumbnail data-URL from an Instagram reel.
 *
 * Mirrors the signature of the legacy Playwright extractor so QueueProcessor
 * needs no contract change. ProgressCallback events use existing types
 * (`status`, `method`, `error`) so the SSE consumers do not need updates.
 */
export async function extractTextAndThumbnail(
	url: string,
	progressCallback?: ProgressCallback
): Promise<ExtractedContent> {
	progressCallback?.({
		type: 'status',
		message: 'Invoking yt-dlp...',
		timestamp: new Date().toISOString()
	});

	const cookies = resolveCookiePath();
	if (cookies) {
		progressCallback?.({
			type: 'status',
			message: `Using cookies from ${cookies}`,
			timestamp: new Date().toISOString()
		});
	}

	const args = [
		'--dump-single-json',
		'--skip-download',
		'--no-warnings',
		'--no-call-home',
		'--socket-timeout',
		'20',
		'--user-agent',
		USER_AGENT,
		...(cookies ? ['--cookies', cookies] : []),
		url
	];

	let stdout: string;
	try {
		const result = await execFileAsync('yt-dlp', args, {
			timeout: YTDLP_TIMEOUT_MS,
			maxBuffer: 10 * 1024 * 1024
		});
		stdout = result.stdout;
	} catch (e: any) {
		const stderr = String(e?.stderr ?? e?.message ?? '');
		const code = e?.code;
		if (code === 'ENOENT') {
			throw new Error(
				'yt-dlp is not installed in this container. Add it to the Dockerfile.'
			);
		}
		const { recoverable, reason } = classifyYtDlpError(stderr);
		progressCallback?.({
			type: 'error',
			message: `yt-dlp failed: ${reason}`,
			timestamp: new Date().toISOString()
		});
		const err = new Error(`yt-dlp extraction failed: ${reason}`);
		// QueueProcessor.isRecoverableError() classifies on message; surface keywords.
		if (!recoverable) (err as any).nonRecoverable = true;
		throw err;
	}

	let data: YtDlpJson;
	try {
		data = JSON.parse(stdout);
	} catch (e) {
		logError('[ytdlp] Failed to parse yt-dlp JSON output', e);
		throw new Error('yt-dlp returned invalid JSON');
	}

	const bodyText = (data.description ?? data.title ?? '').trim();
	if (!bodyText) {
		throw new Error('yt-dlp returned no description for this reel');
	}

	progressCallback?.({
		type: 'status',
		message: `Caption extracted (${bodyText.length} chars)`,
		timestamp: new Date().toISOString()
	});

	let thumbnail: string | null = null;
	const thumbUrl = pickThumbnailUrl(data);
	if (thumbUrl) {
		progressCallback?.({
			type: 'thumbnail',
			message: 'Fetching thumbnail...',
			timestamp: new Date().toISOString()
		});
		thumbnail = await fetchImageAsBase64(thumbUrl);
		progressCallback?.({
			type: 'status',
			message: thumbnail ? 'Thumbnail fetched' : 'Thumbnail fetch failed (continuing without)',
			timestamp: new Date().toISOString()
		});
	}

	progressCallback?.({
		type: 'complete',
		message: 'Extraction complete',
		timestamp: new Date().toISOString()
	});

	return { bodyText, thumbnail };
}
