import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import axios from 'axios';
import yauzl from 'yauzl';
import { AppEnv } from './env';
import { fetchElectronSystemConfig } from './systemConfig';
import Log from './log/main';

let statusListener: ((data: any) => void) | null = null;

export function setResourceStatusListener(listener: ((data: any) => void) | null) {
    statusListener = listener;
}

function emitStatus(data: any) {
    if (statusListener) {
        statusListener(data);
    }
}

export interface ResourceBundleInfo {
    version: string;
    archive: string;
    url: string;
    sha256: string;
    size: number;
    required: boolean;
    extractTo: string;
    source?: string;
}

export interface ResourceManifest {
    manifestVersion: number;
    generatedAt: string;
    appVersion: string;
    bundles: Record<string, ResourceBundleInfo>;
}

interface LocalResourceState {
    manifestVersion: number;
    appVersion: string;
    bundles: Record<string, { version: string; sha256: string; installedAt: string; path: string }>;
}

const getEmbeddedManifestPath = () => {
    const packagedPath = path.join(process.resourcesPath || AppEnv.appRoot, 'extra', 'common', 'resource-manifest.json');
    if (fs.existsSync(packagedPath)) {
        return packagedPath;
    }
    return path.join(AppEnv.appRoot, 'electron', 'resources', 'extra', 'common', 'resource-manifest.json');
};
const getLocalStatePath = () => path.join(AppEnv.resourceStateRoot, 'resource-state.json');
const getBundleRoot = (bundleName: string) => path.join(AppEnv.resourceBundleRoot, bundleName);
const getArchivePath = (bundle: ResourceBundleInfo) => path.join(AppEnv.resourceBundleRoot, bundle.archive);
const getBundleTempRoot = () => path.join(AppEnv.tempRoot, 'resource-install');

function readJsonFile<T>(filePath: string, fallback: T): T {
    try {
        if (!fs.existsSync(filePath)) return fallback;
        return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as T;
    } catch {
        return fallback;
    }
}

function writeJsonFile(filePath: string, data: any) {
    fs.mkdirSync(path.dirname(filePath), { recursive: true });
    fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
}

function rmIfExists(targetPath: string) {
    if (fs.existsSync(targetPath)) {
        fs.rmSync(targetPath, { recursive: true, force: true });
    }
}

function ensureDir(dir: string) {
    fs.mkdirSync(dir, { recursive: true });
}

const bundleSentinels: Record<string, string[]> = {
    'python-runtime': ['python.exe', 'subtitle_cover_generator_simple.py'],
    ffmpeg: ['ffmpeg.exe', 'ffprobe.exe'],
    models: ['u2net.onnx'],
    ziti: ['NotoSerifCJK-VF.ttf.ttc'],
};

function isBundleReady(bundleName: string, bundleRoot: string): boolean {
    if (!fs.existsSync(bundleRoot)) {
        return false;
    }

    try {
        const entries = fs.readdirSync(bundleRoot);
        if (entries.length === 0) {
            return false;
        }
    } catch {
        return false;
    }

    const sentinels = bundleSentinels[bundleName] || [];
    return sentinels.every((relativePath) => fs.existsSync(path.join(bundleRoot, relativePath)));
}
function downloadToFile(url: string, filePath: string, onProgress?: (downloaded: number, total: number) => void): Promise<void> {
    return new Promise((resolve, reject) => {
        ensureDir(path.dirname(filePath));
        const writer = fs.createWriteStream(filePath);
        let settled = false;

        const fail = (error: any) => {
            if (settled) {
                return;
            }
            settled = true;
            try {
                writer.destroy();
            } catch {
                // ignore cleanup failure
            }
            rmIfExists(filePath);
            reject(error);
        };

        axios({ method: 'get', url, responseType: 'stream', timeout: 10 * 60 * 1000 })
            .then((response) => {
                const total = Number(response.headers['content-length'] || 0);
                let downloaded = 0;
                response.data.on('data', (chunk: Buffer) => {
                    downloaded += chunk.length;
                    onProgress?.(downloaded, total);
                });
                response.data.on('error', fail);
                response.data.pipe(writer);
                writer.on('finish', () => {
                    if (settled) {
                        return;
                    }
                    settled = true;
                    resolve();
                });
                writer.on('error', fail);
            })
            .catch(fail);
    });
}
function extractZip(zipPath: string, destination: string): Promise<void> {
    return new Promise((resolve, reject) => {
        ensureDir(destination);
        yauzl.open(zipPath, { lazyEntries: true }, (error, zipFile) => {
            if (error || !zipFile) {
                reject(error || new Error('打开 zip 失败'));
                return;
            }

            zipFile.readEntry();
            zipFile.on('entry', (entry) => {
                const destPath = path.join(destination, entry.fileName);
                if (/\/$/.test(entry.fileName)) {
                    ensureDir(destPath);
                    zipFile.readEntry();
                    return;
                }

                ensureDir(path.dirname(destPath));
                zipFile.openReadStream(entry, (streamError, readStream) => {
                    if (streamError || !readStream) {
                        reject(streamError || new Error('读取 zip entry 失败'));
                        return;
                    }
                    const writeStream = fs.createWriteStream(destPath);
                    readStream.pipe(writeStream);
                    writeStream.on('finish', () => zipFile.readEntry());
                    writeStream.on('error', reject);
                });
            });

            zipFile.on('end', () => resolve());
            zipFile.on('error', reject);
        });
    });
}

function fileSha256(filePath: string): string {
    const hash = crypto.createHash('sha256');
    hash.update(fs.readFileSync(filePath));
    return hash.digest('hex');
}

async function resolveManifest(): Promise<ResourceManifest | null> {
    const embedded = readJsonFile<ResourceManifest | null>(getEmbeddedManifestPath(), null);
    if (embedded) return embedded;

    try {
        const config = await fetchElectronSystemConfig();
        const baseUrl = config.update_url?.replace(/\/$/, '');
        if (!baseUrl) return null;
        const response = await fetch(`${baseUrl}/resource-manifest.json`);
        if (!response.ok) return null;
        return await response.json() as ResourceManifest;
    } catch (error: any) {
        Log.warn('[ResourceManager] resolveManifest failed', error?.message || error);
        return null;
    }
}

async function ensureArchive(bundleName: string, bundle: ResourceBundleInfo): Promise<string> {
    const archivePath = getArchivePath(bundle);
    if (fs.existsSync(archivePath) && fileSha256(archivePath) === bundle.sha256) {
        emitStatus({ status: 'resource-ready', bundle: bundleName, archive: bundle.archive });
        return archivePath;
    }

    if (!/^https?:\/\//i.test(bundle.url)) {
        const config = await fetchElectronSystemConfig();
        const baseUrl = config.update_url?.replace(/\/$/, '');
        if (!baseUrl) {
            throw new Error(`资源 ${bundle.archive} 缺少可下载地址`);
        }
        bundle.url = `${baseUrl}/${bundle.url.replace(/^\//, '')}`;
    }

    emitStatus({ status: 'resource-downloading', bundle: bundleName, archive: bundle.archive, percent: 0, transferred: 0, total: bundle.size || 0 });
    Log.info('[ResourceManager] downloading archive', { archive: bundle.archive, url: bundle.url });
    await downloadToFile(bundle.url, archivePath, (downloaded, total) => {
        const safeTotal = total || bundle.size || 0;
        emitStatus({
            status: 'resource-downloading',
            bundle: bundleName,
            archive: bundle.archive,
            transferred: downloaded,
            total: safeTotal,
            percent: safeTotal > 0 ? Math.round((downloaded / safeTotal) * 100) : 0,
        });
    });

    const sha256 = fileSha256(archivePath);
    if (sha256 !== bundle.sha256) {
        throw new Error(`资源包校验失败: ${bundle.archive}`);
    }

    emitStatus({ status: 'resource-downloaded', bundle: bundleName, archive: bundle.archive, percent: 100 });
    return archivePath;
}

async function installBundle(bundleName: string, bundle: ResourceBundleInfo, state: LocalResourceState) {
    emitStatus({ status: 'resource-installing', bundle: bundleName, archive: bundle.archive, step: 'download' });
    const archivePath = await ensureArchive(bundleName, bundle);
    const installTempRoot = getBundleTempRoot();
    const bundleTempDir = path.join(installTempRoot, `${bundleName}-${Date.now()}`);
    const bundleRoot = getBundleRoot(bundleName);
    const backupDir = `${bundleRoot}.bak`;

    rmIfExists(bundleTempDir);
    ensureDir(bundleTempDir);
    emitStatus({ status: 'resource-installing', bundle: bundleName, archive: bundle.archive, step: 'extract' });
    await extractZip(archivePath, bundleTempDir);

    rmIfExists(backupDir);
    if (fs.existsSync(bundleRoot)) {
        fs.renameSync(bundleRoot, backupDir);
    }

    try {
        ensureDir(path.dirname(bundleRoot));
        emitStatus({ status: 'resource-installing', bundle: bundleName, archive: bundle.archive, step: 'replace' });
        fs.renameSync(bundleTempDir, bundleRoot);
        rmIfExists(backupDir);
    } catch (error) {
        rmIfExists(bundleRoot);
        if (fs.existsSync(backupDir)) {
            fs.renameSync(backupDir, bundleRoot);
        }
        throw error;
    }

    state.bundles[bundleName] = {
        version: bundle.version,
        sha256: bundle.sha256,
        installedAt: new Date().toISOString(),
        path: bundleRoot,
    };
    emitStatus({ status: 'resource-installed', bundle: bundleName, archive: bundle.archive, percent: 100 });
}

export async function checkResourceBundles(options: { installMissing?: boolean } = {}) {
    const manifest = await resolveManifest();
    if (!manifest) {
        return { manifest: null, missingRequired: [], pending: [], installed: [] };
    }

    const state = readJsonFile<LocalResourceState>(getLocalStatePath(), {
        manifestVersion: manifest.manifestVersion,
        appVersion: manifest.appVersion,
        bundles: {},
    });

    const pending: string[] = [];
    const missingRequired: string[] = [];
    const installed: string[] = [];

    ensureDir(AppEnv.resourceBundleRoot);
    ensureDir(AppEnv.resourceStateRoot);
    ensureDir(getBundleTempRoot());

    for (const [bundleName, bundle] of Object.entries(manifest.bundles)) {
        const bundleDir = getBundleRoot(bundleName);
        const local = state.bundles[bundleName];

        const bundleReady = isBundleReady(bundleName, bundleDir);
        if (bundleReady) {
            state.bundles[bundleName] = {
                version: bundle.version,
                sha256: bundle.sha256,
                installedAt: new Date().toISOString(),
                path: bundleDir,
            };
            emitStatus({ status: 'resource-ready', bundle: bundleName, archive: bundle.archive, prebundled: true });
            continue;
        }

        if (local?.version === bundle.version) {
            Log.warn('[ResourceManager] bundle directory invalid, forcing reinstall', { bundleName, bundleDir });
        }
        pending.push(bundleName);
        if (bundle.required) {
            missingRequired.push(bundleName);
        }

        if (options.installMissing) {
            try {
                await installBundle(bundleName, bundle, state);
                installed.push(bundleName);
            } catch (error: any) {
                emitStatus({ status: 'error', message: `bundle ${bundleName}: ${error?.message || String(error)}` });
                Log.error('[ResourceManager] install bundle failed', { bundleName, message: error?.message || String(error) });
                if (bundle.required) {
                    throw error;
                }
            }
        }
    }

    state.manifestVersion = manifest.manifestVersion;
    state.appVersion = manifest.appVersion;
    writeJsonFile(getLocalStatePath(), state);

    return { manifest, missingRequired, pending, installed };
}
