/**
 * 关卡数据库 — 由 Cocos 工程导出（level-prefabs LevelMapData + 编辑器 spawns）
 *
 * - 编辑器 ▶ 预览：从 assets/level-data/ 加载（不在 resources bundle，fetch 不可用）
 * - 主站 / Web 运行：fetch /unity/levels-database.json
 */
import { assetManager, JsonAsset } from 'cc';
import { PREVIEW } from 'cc/env';
import { LevelConfig } from './LevelTypes';
import { LEVEL_ID_BASE } from './LevelIds';

/** assets/level-data/levels-database.json（与 .meta 一致，勿打进 resources） */
const EDITOR_DB_UUID = '114420ca-c8e3-4204-a040-18282ef5e964';

export interface LevelDatabaseFile {
    version: number;
    generatedAt?: string;
    source?: string;
    levelIdBase?: number;
    stats?: { total: number; withPrefabTilemap?: number; withBoundaryRing?: number };
    levels: Record<string, LevelConfig>;
}

export interface LevelsDbIndex {
    version: number;
    mode: 'sharded';
    shardSize?: number;
    levelIdBase?: number;
    generatedAt?: string;
    source?: string;
    total: number;
    min: number;
    max: number;
    stats?: LevelDatabaseFile['stats'];
    shards: Array<{ min: number; max: number; file: string; count?: number }>;
}

declare global {
    interface Window {
        /** 主站 scratch-gui 注入：/unity/levels-database.json */
        __tfrhLevelsDatabaseUrl?: string;
        /** loader 预注入的 JSON（可选，legacy 全量） */
        __tfrhLevelsDatabaseJson?: LevelDatabaseFile;
        /** 分片索引 URL / 预注入 */
        __tfrhLevelsDbIndexUrl?: string;
        __tfrhLevelsDbIndex?: LevelsDbIndex;
        /** loader 按关拉取分片 */
        __tfrhEnsureLevelDbShard?: (levelId: number) => Promise<void>;
    }
}

let fileCache: LevelDatabaseFile | null = null;
let indexCache: LevelsDbIndex | null = null;
let levelsMap: Record<number, LevelConfig> = {};
let sortedIds: number[] = [];
let loadPromise: Promise<void> | null = null;
const loadedShardKeys = new Set<string>();
const shardLoadPromises = new Map<string, Promise<void>>();

function rebuildIndex() {
    sortedIds = Object.keys(levelsMap)
        .map((k) => parseInt(k, 10))
        .filter((n) => !Number.isNaN(n))
        .sort((a, b) => a - b);
}

function ingestFile(data: LevelDatabaseFile, merge = false) {
    if (!merge) {
        fileCache = data;
        levelsMap = {};
    } else if (!fileCache) {
        fileCache = {
            version: data.version || 1,
            levels: {},
        };
    }
    for (const [k, cfg] of Object.entries(data.levels ?? {})) {
        const id = parseInt(k, 10);
        if (!Number.isNaN(id)) {
            levelsMap[id] = { ...cfg, levelID: id };
            if (fileCache?.levels) {
                fileCache.levels[String(id)] = levelsMap[id];
            }
        }
    }
    rebuildIndex();
}

function ingestIndex(data: LevelsDbIndex) {
    indexCache = data;
    if (!fileCache) {
        fileCache = {
            version: data.version,
            generatedAt: data.generatedAt,
            source: data.source,
            levelIdBase: data.levelIdBase,
            stats: data.stats,
            levels: {},
        };
    }
}

function findShardForLevel(levelId: number): LevelsDbIndex['shards'][0] | null {
    if (!indexCache?.shards?.length) return null;
    for (const shard of indexCache.shards) {
        if (levelId >= shard.min && levelId <= shard.max) return shard;
    }
    return null;
}

function shardKey(shard: { file: string }) {
    return shard.file;
}

function validateIndex(index: LevelsDbIndex): void {
    if (index.total < 1 || index.min < LEVEL_ID_BASE) {
        throw new Error(
            `关卡库索引无效 (${index.total} 关)，请重新 package-for-project`,
        );
    }
}

function resolveRemoteUrl(): string | null {
    if (typeof window === 'undefined') return null;
    const url = window.__tfrhLevelsDatabaseUrl;
    return url?.trim() || null;
}

function validateIngested(): void {
    if (indexCache) {
        validateIndex(indexCache);
        return;
    }
    const total = sortedIds.length;
    const minId = sortedIds[0] ?? 0;
    if (total < 1 || minId < LEVEL_ID_BASE) {
        throw new Error(
            `关卡库无效 (${total} 关)，请用主站导出的 levels-database.json；`
            + '运行 bash tools/sync-level-db.sh 后重新 package-for-project',
        );
    }
}

function canUseHttpFetch(): boolean {
    if (typeof window === 'undefined') return false;
    const proto = window.location.protocol;
    return proto === 'http:' || proto === 'https:';
}

function fetchCandidates(): string[] {
    const out: string[] = [];
    const configured = resolveRemoteUrl();
    if (configured && /^https?:\/\//i.test(configured)) {
        out.push(configured);
    }
    if (canUseHttpFetch()) {
        out.push(new URL('levels-database.json', window.location.href).href);
        out.push(new URL('/unity/levels-database.json', window.location.origin).href);
    }
    return [...new Set(out)];
}

function indexFetchCandidates(): string[] {
    const out: string[] = [];
    if (typeof window !== 'undefined') {
        const configured = window.__tfrhLevelsDbIndexUrl?.trim();
        if (configured && /^https?:\/\//i.test(configured)) {
            out.push(configured);
        }
    }
    if (canUseHttpFetch()) {
        out.push(new URL('levels-db-index.json', window.location.href).href);
        out.push(new URL('/unity/levels-db-index.json', window.location.origin).href);
    }
    return [...new Set(out)];
}

async function fetchJsonWithBrotli(url: string): Promise<unknown> {
    const brUrl = url.replace(/\.json(\?.*)?$/i, '.json.br$1');
    try {
        const brRes = await fetch(brUrl);
        if (brRes.ok && typeof DecompressionStream !== 'undefined') {
            const text = await new Response(await brRes.arrayBuffer())
                .pipeThrough(new DecompressionStream('brotli'))
                .text();
            return JSON.parse(text);
        }
    } catch {
        /* fallback */
    }
    const res = await fetch(url, { credentials: 'same-origin' });
    if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
    return res.json();
}

async function loadIndexFromNetwork(): Promise<void> {
    const candidates = indexFetchCandidates();
    if (!candidates.length) {
        throw new Error('[LevelDatabase] 无法 fetch 关卡库索引');
    }
    const errors: unknown[] = [];
    for (const url of candidates) {
        try {
            const json = await fetchJsonWithBrotli(url) as LevelsDbIndex;
            if (json?.mode !== 'sharded' || !json.shards?.length) {
                throw new Error('非分片索引');
            }
            ingestIndex(json);
            validateIngested();
            console.log(
                `[LevelDatabase] 已加载索引 ${json.total} 关 (${json.min}–${json.max})`,
                url,
            );
            return;
        } catch (e) {
            errors.push(e);
        }
    }
    throw new Error(
        `[LevelDatabase] 无法加载关卡库索引: ${candidates.join(', ')}; `
        + errors.map((e) => String(e)).join('; '),
    );
}

function resolveShardUrl(shardFile: string): string {
    const rel = shardFile.replace(/^\/+/, '');
    if (canUseHttpFetch()) {
        return new URL(`/unity/StreamingAssets/aa/${rel}`, window.location.origin).href;
    }
    return rel;
}

async function loadShardFromNetwork(shard: LevelsDbIndex['shards'][0]): Promise<void> {
    const rel = shard.file.replace(/^\/+/, '');
    const url = resolveShardUrl(rel);
    const json = await fetchJsonWithBrotli(url) as LevelDatabaseFile;
    ingestFile(json, true);
    console.log(
        `[LevelDatabase] 已加载分片 ${shard.min}–${shard.max} (+${Object.keys(json.levels ?? {}).length} 关)`,
    );
}

async function loadShardNow(shard: LevelsDbIndex['shards'][0]): Promise<void> {
    const key = shardKey(shard);
    if (loadedShardKeys.has(key)) return;
    const pending = shardLoadPromises.get(key);
    if (pending) {
        await pending;
        return;
    }
    const promise = (async () => {
        if (typeof window !== 'undefined' && window.__tfrhEnsureLevelDbShard) {
            await window.__tfrhEnsureLevelDbShard(shard.min);
            const json = window.__tfrhLevelsDatabaseJson;
            if (json?.levels) {
                const partial: LevelDatabaseFile = { version: json.version || 2, levels: {} };
                for (const [k, cfg] of Object.entries(json.levels)) {
                    const id = parseInt(k, 10);
                    if (!Number.isNaN(id) && id >= shard.min && id <= shard.max) {
                        partial.levels[k] = cfg;
                    }
                }
                if (Object.keys(partial.levels).length) {
                    ingestFile(partial, true);
                    loadedShardKeys.add(key);
                    console.log(
                        `[LevelDatabase] 已合并分片 ${shard.min}–${shard.max} (+${Object.keys(partial.levels).length} 关)`,
                    );
                    return;
                }
            }
        }
        await loadShardFromNetwork(shard);
        loadedShardKeys.add(key);
    })();
    shardLoadPromises.set(key, promise);
    try {
        await promise;
    } finally {
        shardLoadPromises.delete(key);
    }
}

/** 进关前确保该关所在分片已加载 */
export async function ensureLevelShardLoaded(levelId: number): Promise<void> {
    if (!indexCache) return;
    if (levelId in levelsMap) return;
    const shard = findShardForLevel(levelId);
    if (!shard) {
        console.warn(`[LevelDatabase] 关卡 ${levelId} 不在索引范围`);
        return;
    }
    await loadShardNow(shard);
}

function loadFromRemote(url: string): Promise<void> {
    const abs = /^https?:\/\//i.test(url)
        ? url
        : new URL(url, typeof window !== 'undefined' ? window.location.href : 'http://localhost/').href;
    return fetch(abs, { credentials: 'same-origin' })
        .then((res) => {
            if (!res.ok) throw new Error(`HTTP ${res.status} for ${abs}`);
            return res.json() as Promise<LevelDatabaseFile>;
        })
        .then((json) => {
            ingestFile(json);
            validateIngested();
            console.log(
                `[LevelDatabase] 已加载 ${sortedIds.length} 关 (`
                + `${sortedIds[0] ?? '?'}–${sortedIds[sortedIds.length - 1] ?? '?'})`,
                abs,
            );
        });
}

async function loadFromNetwork(): Promise<void> {
    const candidates = fetchCandidates();
    if (!candidates.length) {
        throw new Error('[LevelDatabase] 当前环境无法 fetch，且无预注入关卡库');
    }
    const errors: unknown[] = [];
    for (const url of candidates) {
        try {
            await loadFromRemote(url);
            return;
        } catch (e) {
            errors.push(e);
        }
    }
    const detail = errors.map((e) => String(e)).join('; ');
    throw new Error(
        `[LevelDatabase] 无法加载关卡库，已尝试: ${candidates.join(', ')}; ${detail}`,
    );
}

/** 编辑器预览：packages:// 协议下用 assetManager 读工程内 JSON（不进 resources bundle） */
function loadFromEditorAsset(): Promise<void> {
    return new Promise((resolve, reject) => {
        assetManager.loadAny({ uuid: EDITOR_DB_UUID }, (err: Error | null, asset: JsonAsset) => {
            if (err || !asset?.json) {
                reject(err ?? new Error('assets/level-data/levels-database.json 未找到'));
                return;
            }
            ingestFile(asset.json as LevelDatabaseFile);
            validateIngested();
            console.log(
                `[LevelDatabase] 已加载编辑器关卡库 ${sortedIds.length} 关 (`
                + `${sortedIds[0] ?? '?'}–${sortedIds[sortedIds.length - 1] ?? '?'})`,
            );
            resolve();
        });
    });
}

async function waitForInjection(maxMs = 2500): Promise<LevelDatabaseFile | null> {
    if (typeof window === 'undefined') return null;
    const step = 50;
    const tries = Math.ceil(maxMs / step);
    for (let i = 0; i < tries; i++) {
        const json = window.__tfrhLevelsDatabaseJson;
        if (json) return json;
        await new Promise((r) => setTimeout(r, step));
    }
    return null;
}

/** 异步加载（AppBootstrap 启动时调用；分片模式仅拉 index） */
export function loadLevelDatabase(): Promise<void> {
    if (fileCache || indexCache) return Promise.resolve();
    if (loadPromise) return loadPromise;
    loadPromise = (async () => {
        try {
            if (typeof window !== 'undefined' && window.__tfrhLevelsDbIndex) {
                ingestIndex(window.__tfrhLevelsDbIndex);
                validateIngested();
                console.log(`[LevelDatabase] 已注入索引 ${indexCache!.total} 关`);
                return;
            }

            if (typeof window !== 'undefined' && window.__tfrhLevelsDatabaseJson) {
                ingestFile(window.__tfrhLevelsDatabaseJson);
                validateIngested();
                console.log(`[LevelDatabase] 已注入 ${sortedIds.length} 关`);
                return;
            }

            const injected = PREVIEW ? await waitForInjection(500) : null;
            if (injected) {
                ingestFile(injected);
                validateIngested();
                console.log(`[LevelDatabase] 已注入 ${sortedIds.length} 关`);
                return;
            }

            if (!canUseHttpFetch()) {
                await loadFromEditorAsset();
                return;
            }

            if (PREVIEW) {
                try {
                    await loadFromEditorAsset();
                    return;
                } catch (e) {
                    console.warn('[LevelDatabase] 编辑器资源加载失败，尝试 fetch', e);
                }
            }

            try {
                await loadIndexFromNetwork();
                return;
            } catch (e) {
                console.warn('[LevelDatabase] 分片索引不可用，回退全量库', e);
            }

            await loadFromNetwork();
        } catch (e) {
            loadPromise = null;
            throw e;
        }
    })();
    return loadPromise;
}

export function isLevelDatabaseReady(): boolean {
    return fileCache !== null || indexCache !== null;
}

export function isLevelDatabaseSharded(): boolean {
    return indexCache !== null;
}

// --- 查 ---
export function getLevelConfig(levelID: number): LevelConfig | null {
    return levelsMap[levelID] ?? null;
}

/** 是否在 Cocos 导出的关卡库中 */
export function hasLevel(levelID: number): boolean {
    if (levelID in levelsMap) return true;
    if (indexCache && levelID >= indexCache.min && levelID <= indexCache.max) {
        return true;
    }
    return false;
}

export function getLevelIds(): number[] {
    return [...sortedIds];
}

export function getLevelCount(): number {
    return sortedIds.length;
}

export const MIN_LEVEL_ID = (): number => indexCache?.min ?? sortedIds[0] ?? LEVEL_ID_BASE;
export const MAX_LEVEL_ID = (): number => indexCache?.max ?? sortedIds[sortedIds.length - 1] ?? LEVEL_ID_BASE;

export function nextLevelId(cur: number): number {
    if (sortedIds.length) {
        const i = sortedIds.indexOf(cur);
        if (i < 0) return sortedIds[0] ?? cur;
        return sortedIds[(i + 1) % sortedIds.length];
    }
    const max = MAX_LEVEL_ID();
    const min = MIN_LEVEL_ID();
    if (cur < min) return min;
    if (cur >= max) return min;
    return cur + 1;
}

export function prevLevelId(cur: number): number {
    if (sortedIds.length) {
        const i = sortedIds.indexOf(cur);
        if (i < 0) return sortedIds[0] ?? cur;
        return sortedIds[(i - 1 + sortedIds.length) % sortedIds.length];
    }
    const max = MAX_LEVEL_ID();
    const min = MIN_LEVEL_ID();
    if (cur <= min) return max;
    return cur - 1;
}

// --- 增改（运行时 / 编辑器脚本） ---
export function setLevel(config: LevelConfig): void {
    levelsMap[config.levelID] = { ...config };
    if (fileCache) {
        fileCache.levels[String(config.levelID)] = levelsMap[config.levelID];
    }
    rebuildIndex();
}

export function addLevel(config: LevelConfig): void {
    setLevel(config);
}

/** 删 */
export function removeLevel(levelID: number): boolean {
    if (!(levelID in levelsMap)) return false;
    delete levelsMap[levelID];
    if (fileCache?.levels) {
        delete fileCache.levels[String(levelID)];
    }
    rebuildIndex();
    return true;
}

/** 导出当前内存数据（可写回 JSON 文件） */
export function exportDatabaseJson(): string {
    const payload: LevelDatabaseFile = fileCache ?? {
        version: 1,
        generatedAt: new Date().toISOString(),
        source: 'runtime',
        levels: {},
    };
    payload.levels = Object.fromEntries(
        sortedIds.map((id) => [String(id), levelsMap[id]]),
    );
    payload.stats = { total: sortedIds.length };
    return JSON.stringify(payload, null, 2);
}
