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

import { ClassicLevel } from 'classic-level';

import type {
  FoundryActorDoc,
  FoundryAdventureDoc,
  FoundryFolderDoc,
  FoundryJournalDoc,
  FoundryLoadedDocuments,
  FoundryPlaylistDoc,
  FoundrySceneDoc,
} from '../../shared/foundry/foundryTypes';

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

function asDocArray<T>(docs: unknown[]): T[] {
  return docs.filter((d) => isRecord(d) && typeof d._id === 'string' && typeof d.name === 'string') as T[];
}

/** NeDB: по строке JSON на линию (игнор пустых / битых). */
export async function readNedbDocuments(filePath: string): Promise<Record<string, unknown>[]> {
  const raw = await fs.readFile(filePath, 'utf8');
  const out: Record<string, unknown>[] = [];
  for (const line of raw.split(/\r?\n/u)) {
    const trimmed = line.trim();
    if (!trimmed) continue;
    try {
      const parsed: unknown = JSON.parse(trimmed);
      if (isRecord(parsed) && typeof parsed._id === 'string') out.push(parsed);
    } catch {
      // skip broken line
    }
  }
  return out;
}

function isLevelDbDir(entries: { name: string; isFile(): boolean; isDirectory(): boolean }[]): boolean {
  const names = new Set(entries.map((e) => e.name));
  return (
    names.has('CURRENT') ||
    names.has('LOG') ||
    [...names].some((n) => n.endsWith('.ldb') || n.startsWith('MANIFEST-'))
  );
}

/** Читает primary documents из LevelDB-папки Foundry (ключи `!collection!id`). */
export async function readLevelDbDocuments(dirPath: string): Promise<Record<string, unknown>[]> {
  const db = new ClassicLevel(dirPath, {
    keyEncoding: 'utf8',
    valueEncoding: 'json',
    createIfMissing: false,
  });
  try {
    const out: Record<string, unknown>[] = [];
    for await (const [key, value] of db.iterator()) {
      if (typeof key !== 'string') continue;
      const parts = key.split('!');
      // "", collection, id
      if (parts.length < 3) continue;
      const collection = parts[1] ?? '';
      if (!collection || collection.includes('.')) continue; // embedded
      if (!isRecord(value)) continue;
      if (typeof value._id !== 'string') continue;
      out.push(value);
    }
    return out;
  } finally {
    await db.close().catch(() => undefined);
  }
}

async function pathExists(p: string): Promise<boolean> {
  try {
    await fs.access(p);
    return true;
  } catch {
    return false;
  }
}

async function readCollectionDocs(baseDir: string, collection: string): Promise<Record<string, unknown>[]> {
  const candidates = [
    path.join(baseDir, collection),
    path.join(baseDir, `${collection}.db`),
    path.join(baseDir, 'data', collection),
    path.join(baseDir, 'data', `${collection}.db`),
  ];

  for (const candidate of candidates) {
    if (!(await pathExists(candidate))) continue;
    const st = await fs.stat(candidate);
    if (st.isFile() && candidate.endsWith('.db')) {
      return readNedbDocuments(candidate);
    }
    if (st.isDirectory()) {
      const entries = await fs.readdir(candidate, { withFileTypes: true });
      if (isLevelDbDir(entries)) {
        return readLevelDbDocuments(candidate);
      }
    }
  }
  return [];
}

export async function readPackDocuments(
  packAbsPath: string,
  documentType: string,
): Promise<Record<string, unknown>[]> {
  if (!(await pathExists(packAbsPath))) return [];
  const st = await fs.stat(packAbsPath);
  if (st.isFile() && packAbsPath.endsWith('.db')) {
    return readNedbDocuments(packAbsPath);
  }
  if (st.isDirectory()) {
    const entries = await fs.readdir(packAbsPath, { withFileTypes: true });
    if (isLevelDbDir(entries)) {
      return readLevelDbDocuments(packAbsPath);
    }
    // Иногда path указывает на папку с .db внутри.
    const nestedDb = path.join(packAbsPath, `${path.basename(packAbsPath)}.db`);
    if (await pathExists(nestedDb)) return readNedbDocuments(nestedDb);
  }
  void documentType;
  return [];
}

function mergeById<T extends { _id: string }>(lists: T[][]): T[] {
  const map = new Map<string, T>();
  for (const list of lists) {
    for (const doc of list) {
      if (!map.has(doc._id)) map.set(doc._id, doc);
    }
  }
  return [...map.values()];
}

function flattenAdventureDocs(adventures: FoundryAdventureDoc[]): {
  scenes: FoundrySceneDoc[];
  actors: FoundryActorDoc[];
  playlists: FoundryPlaylistDoc[];
  journals: FoundryJournalDoc[];
  folders: FoundryFolderDoc[];
} {
  const scenes: FoundrySceneDoc[] = [];
  const actors: FoundryActorDoc[] = [];
  const playlists: FoundryPlaylistDoc[] = [];
  const journals: FoundryJournalDoc[] = [];
  const folders: FoundryFolderDoc[] = [];
  for (const adv of adventures) {
    if (Array.isArray(adv.scenes)) scenes.push(...asDocArray<FoundrySceneDoc>(adv.scenes));
    if (Array.isArray(adv.actors)) actors.push(...asDocArray<FoundryActorDoc>(adv.actors));
    if (Array.isArray(adv.playlists)) playlists.push(...asDocArray<FoundryPlaylistDoc>(adv.playlists));
    if (Array.isArray(adv.journal)) journals.push(...asDocArray<FoundryJournalDoc>(adv.journal));
    if (Array.isArray(adv.folders)) {
      for (const f of adv.folders) {
        if (f && typeof f === 'object' && typeof (f as FoundryFolderDoc)._id === 'string') {
          folders.push(f as FoundryFolderDoc);
        }
      }
    }
  }
  return { scenes, actors, playlists, journals, folders };
}

function asFolderArray(docs: unknown[]): FoundryFolderDoc[] {
  return docs.filter(
    (d): d is FoundryFolderDoc =>
      Boolean(d) &&
      typeof d === 'object' &&
      typeof (d as FoundryFolderDoc)._id === 'string' &&
      typeof (d as FoundryFolderDoc).name === 'string',
  );
}

export async function loadWorldDocuments(rootDir: string): Promise<FoundryLoadedDocuments> {
  const [scenesRaw, actorsRaw, playlistsRaw, journalsRaw, adventuresRaw, foldersRaw] =
    await Promise.all([
      readCollectionDocs(rootDir, 'scenes'),
      readCollectionDocs(rootDir, 'actors'),
      readCollectionDocs(rootDir, 'playlists'),
      readCollectionDocs(rootDir, 'journal'),
      readCollectionDocs(rootDir, 'adventures'),
      readCollectionDocs(rootDir, 'folders'),
    ]);

  const adventures = asDocArray<FoundryAdventureDoc>(adventuresRaw);
  const embedded = flattenAdventureDocs(adventures);

  return {
    scenes: mergeById([asDocArray<FoundrySceneDoc>(scenesRaw), embedded.scenes]),
    actors: mergeById([asDocArray<FoundryActorDoc>(actorsRaw), embedded.actors]),
    playlists: mergeById([asDocArray<FoundryPlaylistDoc>(playlistsRaw), embedded.playlists]),
    journals: mergeById([asDocArray<FoundryJournalDoc>(journalsRaw), embedded.journals]),
    adventures,
    folders: mergeById([asFolderArray(foldersRaw), embedded.folders]),
  };
}

export async function loadModuleDocuments(
  rootDir: string,
  packs: { path: string; type: string }[],
): Promise<FoundryLoadedDocuments> {
  const scenes: FoundrySceneDoc[][] = [];
  const actors: FoundryActorDoc[][] = [];
  const playlists: FoundryPlaylistDoc[][] = [];
  const journals: FoundryJournalDoc[][] = [];
  const adventures: FoundryAdventureDoc[][] = [];

  for (const pack of packs) {
    const abs = path.isAbsolute(pack.path) ? pack.path : path.join(rootDir, pack.path);
    // path в module.json иногда с `./` и иногда ещё со старым `.db`
    const variants = [abs, abs.endsWith('.db') ? abs : `${abs}.db`, abs.replace(/\.db$/u, '')];
    let docs: Record<string, unknown>[] = [];
    for (const v of variants) {
      docs = await readPackDocuments(v, pack.type);
      if (docs.length > 0) break;
    }

    const type = pack.type;
    if (type === 'Scene') scenes.push(asDocArray<FoundrySceneDoc>(docs));
    else if (type === 'Actor') actors.push(asDocArray<FoundryActorDoc>(docs));
    else if (type === 'Playlist') playlists.push(asDocArray<FoundryPlaylistDoc>(docs));
    else if (type === 'JournalEntry') journals.push(asDocArray<FoundryJournalDoc>(docs));
    else if (type === 'Adventure') adventures.push(asDocArray<FoundryAdventureDoc>(docs));
  }

  const advMerged = mergeById(adventures);
  const embedded = flattenAdventureDocs(advMerged);

  return {
    scenes: mergeById([...scenes, embedded.scenes]),
    actors: mergeById([...actors, embedded.actors]),
    playlists: mergeById([...playlists, embedded.playlists]),
    journals: mergeById([...journals, embedded.journals]),
    adventures: advMerged,
    folders: mergeById([embedded.folders]),
  };
}
