import fs from 'node:fs/promises';
import path from 'node:path';

import { decodeFoundryAssetPath } from '../../shared/foundry/foundryPaths';
import type { FoundryPackageManifest, FoundryPackRef } from '../../shared/foundry/foundryTypes';
import { isSupportedFoundryPackage } from '../../shared/foundry/foundryVersion';

function isRecord(v: unknown): v is Record<string, unknown> {
  return Boolean(v) && typeof v === 'object' && !Array.isArray(v);
}

async function readJsonFile(filePath: string): Promise<unknown> {
  const raw = await fs.readFile(filePath, 'utf8');
  return JSON.parse(raw) as unknown;
}

function parsePacks(raw: unknown): FoundryPackRef[] {
  if (!Array.isArray(raw)) return [];
  const out: FoundryPackRef[] = [];
  for (const item of raw) {
    if (!isRecord(item)) continue;
    const name = typeof item.name === 'string' ? item.name : '';
    const label = typeof item.label === 'string' ? item.label : name;
    const packPath = typeof item.path === 'string' ? item.path.replace(/^\.\//u, '') : '';
    const type =
      typeof item.type === 'string' ? item.type : typeof item.entity === 'string' ? item.entity : '';
    if (!packPath || !type) continue;
    out.push({ name: name || path.basename(packPath), label: label || name, path: packPath, type });
  }
  return out;
}

function titleFromManifest(data: Record<string, unknown>, fallbackId: string): string {
  if (typeof data.title === 'string' && data.title.trim()) return data.title.trim();
  if (typeof data.name === 'string' && data.name.trim()) return data.name.trim();
  return fallbackId;
}

function idFromManifest(data: Record<string, unknown>, dirName: string): string {
  if (typeof data.id === 'string' && data.id.trim()) return data.id.trim();
  if (typeof data.name === 'string' && data.name.trim()) return data.name.trim();
  return dirName;
}

async function tryParseManifest(dir: string): Promise<FoundryPackageManifest | null> {
  const worldPath = path.join(dir, 'world.json');
  const modulePath = path.join(dir, 'module.json');

  try {
    await fs.access(worldPath);
    const data = await readJsonFile(worldPath);
    if (!isRecord(data)) return null;
    const id = idFromManifest(data, path.basename(dir));
    const manifest: FoundryPackageManifest = {
      kind: 'world',
      id,
      title: titleFromManifest(data, id),
      rootDir: dir,
      packs: [],
    };
    const compatibility = parseCompatibility(data.compatibility);
    if (compatibility) manifest.compatibility = compatibility;
    if (typeof data.coreVersion === 'string') manifest.coreVersion = data.coreVersion;
    return manifest;
  } catch {
    // not a world
  }

  try {
    await fs.access(modulePath);
    const data = await readJsonFile(modulePath);
    if (!isRecord(data)) return null;
    const id = idFromManifest(data, path.basename(dir));
    const manifest: FoundryPackageManifest = {
      kind: 'module',
      id,
      title: titleFromManifest(data, id),
      rootDir: dir,
      packs: parsePacks(data.packs),
    };
    const compatibility = parseCompatibility(data.compatibility);
    if (compatibility) manifest.compatibility = compatibility;
    if (typeof data.coreVersion === 'string') manifest.coreVersion = data.coreVersion;
    return manifest;
  } catch {
    return null;
  }
}

function parseCompatibility(raw: unknown): FoundryPackageManifest['compatibility'] | null {
  if (!isRecord(raw)) return null;
  const out: NonNullable<FoundryPackageManifest['compatibility']> = {};
  if (typeof raw.minimum === 'string') out.minimum = raw.minimum;
  if (typeof raw.verified === 'string') out.verified = raw.verified;
  if (typeof raw.maximum === 'string') out.maximum = raw.maximum;
  return out;
}

/** Ищет world.json / module.json в папке и на 1–2 уровня глубже. */
export async function detectFoundryPackage(rootPath: string): Promise<FoundryPackageManifest> {
  const abs = path.resolve(rootPath);
  const direct = await tryParseManifest(abs);
  if (direct) {
    const ver = isSupportedFoundryPackage(direct);
    if (!ver.ok) throw new Error(ver.reason ?? 'Unsupported Foundry version');
    return direct;
  }

  // Архив мог содержать один корневой каталог.
  const entries = await fs.readdir(abs, { withFileTypes: true });
  const dirs = entries.filter((e) => e.isDirectory()).map((e) => path.join(abs, e.name));

  for (const dir of dirs) {
    const found = await tryParseManifest(dir);
    if (found) {
      const ver = isSupportedFoundryPackage(found);
      if (!ver.ok) throw new Error(ver.reason ?? 'Unsupported Foundry version');
      return found;
    }
  }

  // Data/worlds/<id> или Data/modules/<id>
  for (const mid of ['worlds', 'modules']) {
    const midDir = path.join(abs, mid);
    try {
      const st = await fs.stat(midDir);
      if (!st.isDirectory()) continue;
      const children = await fs.readdir(midDir, { withFileTypes: true });
      for (const child of children.filter((c) => c.isDirectory())) {
        const found = await tryParseManifest(path.join(midDir, child.name));
        if (found) {
          const ver = isSupportedFoundryPackage(found);
          if (!ver.ok) throw new Error(ver.reason ?? 'Unsupported Foundry version');
          return found;
        }
      }
    } catch {
      // ignore
    }
  }

  // Data как корень выбранной папки
  const dataDir = path.join(abs, 'Data');
  try {
    const st = await fs.stat(dataDir);
    if (st.isDirectory()) {
      return await detectFoundryPackage(dataDir);
    }
  } catch {
    // ignore
  }

  throw new Error(
    'Не похоже на пакет Foundry VTT: не найдены world.json или module.json. Выберите папку мира/модуля или архив с ними.',
  );
}

/**
 * Резолвит путь ассета Foundry относительно корня пакета / Data.
 * foundryPath — как в документах: `worlds/id/...`, `modules/id/...` или относительный.
 * Поддерживает URL-encoding (`The%20Withered%20Grove%20(day).webp`).
 */
export async function resolveFoundryAssetPath(
  manifest: FoundryPackageManifest,
  foundryPath: string,
): Promise<string | null> {
  const cleaned = decodeFoundryAssetPath(foundryPath);
  if (!cleaned) return null;
  if (/^https?:\/\//iu.test(cleaned)) return null;

  const packagePrefix = manifest.kind === 'world' ? `worlds/${manifest.id}/` : `modules/${manifest.id}/`;

  const rawNormalized = foundryPath.trim().replace(/\\/gu, '/');
  const pathVariants = cleaned === rawNormalized ? [cleaned] : [cleaned, rawNormalized];

  const candidates: string[] = [];
  for (const variant of pathVariants) {
    const v = variant.replace(/^\/+/u, '');
    if (!v) continue;
    if (v.toLowerCase().startsWith(packagePrefix.toLowerCase())) {
      candidates.push(path.join(manifest.rootDir, v.slice(packagePrefix.length)));
    }
    candidates.push(path.join(manifest.rootDir, v));

    const dataRootGuesses = [
      path.resolve(manifest.rootDir, '..', '..'),
      path.resolve(manifest.rootDir, '..'),
    ];
    for (const dataRoot of dataRootGuesses) {
      candidates.push(path.join(dataRoot, v));
    }

    const base = path.basename(v);
    if (base && base !== v) {
      candidates.push(path.join(manifest.rootDir, base));
      candidates.push(path.join(manifest.rootDir, 'assets', base));
    }
  }

  for (const c of candidates) {
    try {
      const st = await fs.stat(c);
      if (st.isFile()) return c;
    } catch {
      // next
    }
  }
  return null;
}
