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

import {
  actorPortraitSrc,
  extractActorDescriptionHtml,
  filterScenesForImport,
  journalDescriptionHtml,
  planFoundrySceneGraph,
  sceneBackgroundSrc,
} from '../../shared/foundry/foundryGraph';
import type {
  FoundryFolderDoc,
  FoundryLoadedDocuments,
  FoundryPackageManifest,
  FoundryPlaylistDoc,
  FoundrySceneDoc,
} from '../../shared/foundry/foundryTypes';
import { DEFAULT_NPC_GROUP_COLOR, normalizeHexColor } from '../../shared/npcs/npcGroups';
import type {
  MediaAsset,
  Project,
  ProjectNpc,
  ProjectNpcGroup,
  Scene,
  SceneGraphEdge,
  SceneGraphNode,
} from '../../shared/types';
import { PROJECT_SCHEMA_VERSION } from '../../shared/types';
import type { AssetId, GraphNodeId, NpcGroupId, SceneId } from '../../shared/types/ids';
import {
  asAssetId,
  asGraphNodeId,
  asNpcGroupId,
  asNpcId,
  asProjectId,
  asSceneId,
} from '../../shared/types/ids';
import { optimizeImageBufferVisuallyLossless } from '../project/optimizeImageImport.lib.mjs';
import { generateScenePreviewThumbnailBytes } from '../project/scenePreviewThumbnail';
import { unzipToDir } from '../project/yauzlProjectZip';
import { getAppSemanticVersion } from '../versionInfo';

import { loadModuleDocuments, loadWorldDocuments } from './foundryDb';
import { detectFoundryPackage, resolveFoundryAssetPath } from './foundryDetect';

export type FoundryImportProgress = {
  stage: 'copy' | 'unzip' | 'zip' | 'done';
  percent: number;
  detail?: string;
};

/** Минимальный 1×1 PNG (серый), если у актёра нет портрета на диске. */
const PLACEHOLDER_PNG = Buffer.from(
  'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==',
  'base64',
);

type MediaKind = { type: 'image' | 'video' | 'audio'; mime: string };

function classifyMediaPath(filePath: string): MediaKind | null {
  const ext = path.extname(filePath).toLowerCase();
  switch (ext) {
    case '.png':
      return { type: 'image', mime: 'image/png' };
    case '.jpg':
    case '.jpeg':
      return { type: 'image', mime: 'image/jpeg' };
    case '.webp':
      return { type: 'image', mime: 'image/webp' };
    case '.gif':
      return { type: 'image', mime: 'image/gif' };
    case '.bmp':
      return { type: 'image', mime: 'image/bmp' };
    case '.mp4':
      return { type: 'video', mime: 'video/mp4' };
    case '.webm':
      return { type: 'video', mime: 'video/webm' };
    case '.mov':
      return { type: 'video', mime: 'video/quicktime' };
    case '.mp3':
      return { type: 'audio', mime: 'audio/mpeg' };
    case '.wav':
      return { type: 'audio', mime: 'audio/wav' };
    case '.ogg':
      return { type: 'audio', mime: 'audio/ogg' };
    case '.m4a':
      return { type: 'audio', mime: 'audio/mp4' };
    case '.aac':
      return { type: 'audio', mime: 'audio/aac' };
    case '.flac':
      return { type: 'audio', mime: 'audio/flac' };
    default:
      return null;
  }
}

function sanitizeFileName(name: string): string {
  const cleaned = name
    .split('')
    .map((ch) => {
      const code = ch.charCodeAt(0);
      if (code < 32 || '<>:"/\\|?*'.includes(ch)) return '_';
      return ch;
    })
    .join('')
    .trim();
  return cleaned.length > 0 ? cleaned.slice(0, 180) : 'file';
}

function randomId(): string {
  return crypto.randomBytes(16).toString('hex');
}

function buildMediaAsset(
  id: AssetId,
  kind: MediaKind,
  originalName: string,
  relPath: string,
  sha256: string,
  sizeBytes: number,
): MediaAsset {
  const createdAt = new Date().toISOString();
  const base = { id, mime: kind.mime, originalName, relPath, sha256, sizeBytes, createdAt };
  if (kind.type === 'image') return { ...base, type: 'image' };
  if (kind.type === 'video') return { ...base, type: 'video' };
  return { ...base, type: 'audio' };
}

async function isZipFile(filePath: string): Promise<boolean> {
  try {
    const st = await fs.stat(filePath);
    if (!st.isFile()) return false;
    const ext = path.extname(filePath).toLowerCase();
    return ext === '.zip' || ext === '.fvtt';
  } catch {
    return false;
  }
}

async function prepareSourceDir(
  sourcePath: string,
  onProgress?: (p: FoundryImportProgress) => void,
): Promise<{ workDir: string; cleanup: () => Promise<void> }> {
  const abs = path.resolve(sourcePath);
  if (await isZipFile(abs)) {
    const workDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ttrpg-foundry-'));
    onProgress?.({ stage: 'unzip', percent: 5, detail: 'Распаковка архива Foundry…' });
    await unzipToDir(abs, workDir, (done, total) => {
      const pct = total > 0 ? 5 + Math.round((done / total) * 25) : 15;
      onProgress?.({ stage: 'unzip', percent: Math.min(30, pct), detail: 'Распаковка архива Foundry…' });
    });
    return {
      workDir,
      cleanup: async () => {
        await fs.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
      },
    };
  }
  const st = await fs.stat(abs);
  if (!st.isDirectory()) {
    throw new Error('Выберите папку мира/модуля Foundry или архив (.zip / .fvtt).');
  }
  return {
    workDir: abs,
    cleanup: () => Promise.resolve(),
  };
}

type AssetWriteCtx = {
  cacheDir: string;
  assets: Record<AssetId, MediaAsset>;
  /** foundry path or abs path → assetId */
  bySourceKey: Map<string, AssetId>;
};

async function importFileAsAsset(
  ctx: AssetWriteCtx,
  absPath: string,
  opts?: { optimizeImage?: boolean; forceKind?: MediaKind },
): Promise<AssetId | null> {
  const key = path.resolve(absPath).toLowerCase();
  const existing = ctx.bySourceKey.get(key);
  if (existing) return existing;

  let kind = opts?.forceKind ?? classifyMediaPath(absPath);
  if (!kind) return null;

  let buf = await fs.readFile(absPath);
  if (kind.type === 'image' && opts?.optimizeImage !== false) {
    try {
      const opt = await optimizeImageBufferVisuallyLossless(buf);
      if (!opt.passthrough && opt.buffer.length > 0) {
        buf = Buffer.from(opt.buffer);
        kind = { type: 'image', mime: opt.mime };
      }
    } catch {
      // keep original
    }
  }

  const id = asAssetId(randomId());
  const orig = path.basename(absPath);
  const safeOrig = sanitizeFileName(orig);
  const relPath = `assets/${id}_${safeOrig}`;
  const absOut = path.join(ctx.cacheDir, relPath);
  await fs.mkdir(path.dirname(absOut), { recursive: true });
  await fs.writeFile(absOut, buf);
  const sha256 = crypto.createHash('sha256').update(buf).digest('hex');
  ctx.assets[id] = buildMediaAsset(id, kind, orig, relPath, sha256, buf.length);
  ctx.bySourceKey.set(key, id);
  return id;
}

async function importPlaceholderAvatar(ctx: AssetWriteCtx, name: string): Promise<AssetId> {
  const id = asAssetId(randomId());
  const orig = `${sanitizeFileName(name)}_avatar.png`;
  const relPath = `assets/${id}_${orig}`;
  const absOut = path.join(ctx.cacheDir, relPath);
  await fs.mkdir(path.dirname(absOut), { recursive: true });
  await fs.writeFile(absOut, PLACEHOLDER_PNG);
  const sha256 = crypto.createHash('sha256').update(PLACEHOLDER_PNG).digest('hex');
  ctx.assets[id] = buildMediaAsset(
    id,
    { type: 'image', mime: 'image/png' },
    orig,
    relPath,
    sha256,
    PLACEHOLDER_PNG.length,
  );
  return id;
}

async function resolveAndImport(
  ctx: AssetWriteCtx,
  manifest: FoundryPackageManifest,
  foundryPath: string | null | undefined,
  opts?: { optimizeImage?: boolean },
): Promise<AssetId | null> {
  if (!foundryPath?.trim()) return null;
  const abs = await resolveFoundryAssetPath(manifest, foundryPath);
  if (!abs) return null;
  return importFileAsAsset(ctx, abs, opts);
}

function uniqueNpcName(base: string, used: Set<string>): string {
  const root = base.trim() || 'NPC';
  if (!used.has(root.toLowerCase())) {
    used.add(root.toLowerCase());
    return root;
  }
  let i = 2;
  while (used.has(`${root} (${String(i)})`.toLowerCase())) i += 1;
  const name = `${root} (${String(i)})`;
  used.add(name.toLowerCase());
  return name;
}

function collectSceneAudioPaths(
  scene: FoundrySceneDoc,
  playlistsById: Map<string, FoundryPlaylistDoc>,
): string[] {
  const paths: string[] = [];
  const playlistId = typeof scene.playlist === 'string' ? scene.playlist : null;
  if (!playlistId) return paths;
  const playlist = playlistsById.get(playlistId);
  if (!playlist?.sounds?.length) return paths;

  const soundId = typeof scene.playlistSound === 'string' ? scene.playlistSound : null;
  const sounds = soundId
    ? playlist.sounds.filter((s) => s._id === soundId)
    : [...playlist.sounds].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));

  for (const s of sounds) {
    if (typeof s.path === 'string' && s.path.trim()) paths.push(s.path.trim());
  }
  return paths;
}

export type FoundryBuiltProject = {
  project: Project;
  /** Абсолютные пути исходников превью для последующей генерации thumb (sceneId → abs path). */
  previewSources: { sceneId: SceneId; absPath: string }[];
};

export async function buildProjectFromFoundryDocuments(
  manifest: FoundryPackageManifest,
  docs: FoundryLoadedDocuments,
  cacheDir: string,
  onProgress?: (p: FoundryImportProgress) => void,
  options?: { projectId?: ReturnType<typeof asProjectId> },
): Promise<FoundryBuiltProject> {
  const projectId = options?.projectId ?? asProjectId(randomId());
  const now = new Date().toISOString();
  const appVer = getAppSemanticVersion();
  const name = manifest.title.trim() || manifest.id;
  const fileBaseName = `${sanitizeFileName(name)}_${projectId}`;

  const ctx: AssetWriteCtx = { cacheDir, assets: {}, bySourceKey: new Map() };
  const journalsById = new Map(docs.journals.map((j) => [j._id, j]));
  const playlistsById = new Map(docs.playlists.map((p) => [p._id, p]));

  const importScenes = filterScenesForImport(docs.scenes);
  const graphPlan = planFoundrySceneGraph(importScenes, docs.adventures, docs.journals);
  const sceneOrderIds =
    graphPlan.orderedSceneIds.length > 0 ? graphPlan.orderedSceneIds : importScenes.map((s) => s._id);

  const scenesByFoundryId = new Map(importScenes.map((s) => [s._id, s]));
  const foundryToSceneId = new Map<string, SceneId>();
  const scenes: Record<SceneId, Scene> = {};
  const sceneListOrder: SceneId[] = [];
  const previewSources: { sceneId: SceneId; absPath: string }[] = [];

  const totalSteps = Math.max(1, sceneOrderIds.length + docs.actors.length);
  let step = 0;

  for (const foundrySceneId of sceneOrderIds) {
    const doc = scenesByFoundryId.get(foundrySceneId);
    if (!doc) continue;
    step += 1;
    onProgress?.({
      stage: 'copy',
      percent: 30 + Math.round((step / totalSteps) * 50),
      detail: `Сцена: ${doc.name}`,
    });

    const sceneId = asSceneId(`s_${randomId()}`);
    foundryToSceneId.set(foundrySceneId, sceneId);

    let previewAssetId: AssetId | null = null;
    let previewAssetType: 'image' | 'video' | null = null;
    let previewThumbAssetId: AssetId | null = null;

    const bg = sceneBackgroundSrc(doc);
    if (bg) {
      const abs = await resolveFoundryAssetPath(manifest, bg);
      if (abs) {
        const kind = classifyMediaPath(abs);
        if (kind?.type === 'image' || kind?.type === 'video') {
          previewAssetId = await importFileAsAsset(ctx, abs, { optimizeImage: kind.type === 'image' });
          previewAssetType = kind.type;
          previewSources.push({ sceneId, absPath: abs });
          if (previewAssetId && kind.type === 'image') {
            try {
              const thumbBytes = await generateScenePreviewThumbnailBytes(abs, 'image');
              if (thumbBytes && thumbBytes.length > 0) {
                const thumbId = asAssetId(randomId());
                const thumbRel = `assets/${thumbId}_preview_thumb.webp`;
                await fs.writeFile(path.join(cacheDir, thumbRel), thumbBytes);
                ctx.assets[thumbId] = buildMediaAsset(
                  thumbId,
                  { type: 'image', mime: 'image/webp' },
                  `${sanitizeFileName(doc.name)}_preview_thumb.webp`,
                  thumbRel,
                  crypto.createHash('sha256').update(thumbBytes).digest('hex'),
                  thumbBytes.length,
                );
                previewThumbAssetId = thumbId;
              }
            } catch {
              // optional
            }
          }
        }
      }
    }

    const audioRefs: Scene['media']['audios'] = [];
    for (const audioPath of collectSceneAudioPaths(doc, playlistsById)) {
      const assetId = await resolveAndImport(ctx, manifest, audioPath);
      if (!assetId) continue;
      if (ctx.assets[assetId]?.type !== 'audio') continue;
      audioRefs.push({ assetId, autoplay: true, loop: true });
    }

    scenes[sceneId] = {
      id: sceneId,
      title: doc.name.trim() || 'Scene',
      description: journalDescriptionHtml(doc, journalsById),
      previewAssetId,
      previewAssetType,
      previewThumbAssetId,
      previewVideoAutostart: previewAssetType === 'video',
      previewRotationDeg: 0,
      darkenScene: false,
      traps: [],
      tokens: [],
      npcTokens: [],
      grid: { enabled: false, type: 'square', sizeN: 0.06, color: '#ffffff' },
      media: { videos: [], audios: audioRefs },
      settings: {
        autoplayVideo: previewAssetType === 'video',
        autoplayAudio: true,
        loopVideo: true,
        loopAudio: true,
      },
      connections: [],
      layout: { x: 0, y: 0 },
    };
    sceneListOrder.push(sceneId);
  }

  const usedPlaylistIds = new Set(
    importScenes.map((s) => s.playlist).filter((id): id is string => typeof id === 'string'),
  );
  const campaignAudios: Project['campaignAudios'] = [];
  for (const pl of docs.playlists) {
    if (usedPlaylistIds.has(pl._id)) continue;
    const sounds = [...(pl.sounds ?? [])].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
    for (const s of sounds) {
      if (typeof s.path !== 'string' || !s.path.trim()) continue;
      const assetId = await resolveAndImport(ctx, manifest, s.path);
      if (!assetId || ctx.assets[assetId]?.type !== 'audio') continue;
      campaignAudios.push({ assetId, autoplay: false, loop: true });
    }
  }

  const { npcGroups, foundryFolderToGroupId } = buildNpcGroupsFromFoundryFolders(docs.folders);

  const npcs: ProjectNpc[] = [];
  const usedNpcNames = new Set<string>();
  let npcIndex = 0;
  for (const actor of docs.actors) {
    // Группы актёров Foundry (party/group) — не персонажи.
    if (actor.type === 'group') continue;
    step += 1;
    onProgress?.({
      stage: 'copy',
      percent: 30 + Math.round((step / totalSteps) * 50),
      detail: `НПС: ${actor.name}`,
    });
    const portrait = actorPortraitSrc(actor);
    let avatarAssetId = portrait ? await resolveAndImport(ctx, manifest, portrait) : null;
    if (!avatarAssetId || ctx.assets[avatarAssetId]?.type !== 'image') {
      avatarAssetId = await importPlaceholderAvatar(ctx, actor.name);
    }
    const npcName = uniqueNpcName(actor.name, usedNpcNames);
    const folderId = typeof actor.folder === 'string' ? actor.folder : null;
    const groupId = folderId ? (foundryFolderToGroupId.get(folderId) ?? null) : null;
    npcs.push({
      id: asNpcId(`npc_${randomId()}`),
      name: npcName,
      avatarAssetId,
      description: extractActorDescriptionHtml(actor),
      x: 80 + (npcIndex % 4) * 220,
      y: 80 + Math.floor(npcIndex / 4) * 200,
      groupId,
      ringColor: '#c9a227',
      disposition: 'neutral',
      imageOffset: { x: 0, y: 0 },
      imageScale: 1,
    });
    npcIndex += 1;
  }

  const sceneGraphNodes: SceneGraphNode[] = [];
  const sceneGraphEdges: SceneGraphEdge[] = [];
  const foundryToGraphNode = new Map<string, GraphNodeId>();

  const startFoundryId = graphPlan.startSceneId;
  const startSceneId =
    (startFoundryId ? foundryToSceneId.get(startFoundryId) : null) ?? sceneListOrder[0] ?? null;

  sceneListOrder.forEach((sceneId, index) => {
    const foundryId = [...foundryToSceneId.entries()].find(([, sid]) => sid === sceneId)?.[0];
    const col = index % 4;
    const row = Math.floor(index / 4);
    const gnId = asGraphNodeId(`gn_${randomId()}`);
    sceneGraphNodes.push({
      id: gnId,
      sceneId,
      x: 80 + col * 280,
      y: 80 + row * 200,
      isStartScene: startSceneId !== null && sceneId === startSceneId,
      isSideStoryStart: false,
      sideStoryLineTitle: '',
    });
    if (foundryId) foundryToGraphNode.set(foundryId, gnId);
  });

  for (const edge of graphPlan.edges) {
    const source = foundryToGraphNode.get(edge.sourceId);
    const target = foundryToGraphNode.get(edge.targetId);
    if (!source || !target || source === target) continue;
    sceneGraphEdges.push({
      id: `e_${randomId()}`,
      sourceGraphNodeId: source,
      targetGraphNodeId: target,
    });
  }

  // connections из рёбер графа
  const outgoing = new Map<SceneId, Set<SceneId>>();
  const gnMap = new Map(sceneGraphNodes.map((n) => [n.id, n]));
  for (const e of sceneGraphEdges) {
    const a = gnMap.get(e.sourceGraphNodeId);
    const b = gnMap.get(e.targetGraphNodeId);
    if (!a || !b || a.sceneId === b.sceneId) continue;
    let set = outgoing.get(a.sceneId);
    if (!set) {
      set = new Set();
      outgoing.set(a.sceneId, set);
    }
    set.add(b.sceneId);
  }
  for (const [sid, set] of outgoing) {
    const sc = scenes[sid];
    if (sc) sc.connections = [...set];
  }

  const startGraphNodeId = sceneGraphNodes.find((n) => n.isStartScene)?.id ?? null;

  const project: Project = {
    id: projectId,
    meta: {
      name,
      fileBaseName,
      createdAt: now,
      updatedAt: now,
      createdWithAppVersion: appVer,
      appVersion: appVer,
      schemaVersion: PROJECT_SCHEMA_VERSION,
    },
    scenes,
    sceneListOrder,
    assets: ctx.assets,
    campaignAudios,
    materials: [],
    npcs,
    npcGroups,
    npcRelations: [],
    currentSceneId: startSceneId,
    currentGraphNodeId: startGraphNodeId,
    sceneGraphNodes,
    sceneGraphEdges,
  };

  return { project, previewSources };
}

/** Actor folders Foundry → дерево групп НПС. */
function buildNpcGroupsFromFoundryFolders(folders: FoundryFolderDoc[]): {
  npcGroups: ProjectNpcGroup[];
  foundryFolderToGroupId: Map<string, NpcGroupId>;
} {
  const actorFolders = folders
    .filter((f) => !f.type || f.type === 'Actor')
    .sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0) || a.name.localeCompare(b.name));

  const foundryFolderToGroupId = new Map<string, NpcGroupId>();
  for (const f of actorFolders) {
    foundryFolderToGroupId.set(f._id, asNpcGroupId(`ng_${randomId()}`));
  }

  const nameKeysByParent = new Map<string | null, Set<string>>();
  const npcGroups: ProjectNpcGroup[] = [];

  // Parents first
  const remaining = [...actorFolders];
  const placed = new Set<string>();
  while (remaining.length > 0) {
    let progress = false;
    for (let i = 0; i < remaining.length; i += 1) {
      const f = remaining[i]!;
      const parentFoundry = typeof f.folder === 'string' && f.folder.trim() ? f.folder.trim() : null;
      if (parentFoundry && !foundryFolderToGroupId.has(parentFoundry)) {
        // orphan parent ref → root
      } else if (parentFoundry && !placed.has(parentFoundry) && foundryFolderToGroupId.has(parentFoundry)) {
        continue;
      }
      const parentId = parentFoundry ? (foundryFolderToGroupId.get(parentFoundry) ?? null) : null;
      const parentKey = parentId;
      let keys = nameKeysByParent.get(parentKey);
      if (!keys) {
        keys = new Set();
        nameKeysByParent.set(parentKey, keys);
      }
      let name = f.name.trim() || 'Group';
      const base = name.toLowerCase();
      if (keys.has(base)) {
        let n = 2;
        while (keys.has(`${base} (${String(n)})`)) n += 1;
        name = `${name} (${String(n)})`;
      }
      keys.add(name.toLowerCase());
      const id = foundryFolderToGroupId.get(f._id)!;
      npcGroups.push({
        id,
        name,
        color: normalizeHexColor(f.color, DEFAULT_NPC_GROUP_COLOR),
        parentId,
      });
      placed.add(f._id);
      remaining.splice(i, 1);
      progress = true;
      break;
    }
    if (!progress) {
      // cycle / leftover → force as roots
      for (const f of remaining) {
        const id = foundryFolderToGroupId.get(f._id)!;
        let name = f.name.trim() || 'Group';
        const keys = nameKeysByParent.get(null) ?? new Set();
        nameKeysByParent.set(null, keys);
        if (keys.has(name.toLowerCase())) {
          let n = 2;
          while (keys.has(`${name.toLowerCase()} (${String(n)})`)) n += 1;
          name = `${name} (${String(n)})`;
        }
        keys.add(name.toLowerCase());
        npcGroups.push({
          id,
          name,
          color: normalizeHexColor(f.color, DEFAULT_NPC_GROUP_COLOR),
          parentId: null,
        });
      }
      break;
    }
  }

  return { npcGroups, foundryFolderToGroupId };
}

export async function loadFoundryDocumentsForImport(
  sourcePath: string,
  onProgress?: (p: FoundryImportProgress) => void,
): Promise<{
  manifest: FoundryPackageManifest;
  docs: FoundryLoadedDocuments;
  cleanup: () => Promise<void>;
}> {
  const prepared = await prepareSourceDir(sourcePath, onProgress);
  try {
    onProgress?.({ stage: 'copy', percent: 32, detail: 'Определение пакета Foundry…' });
    const manifest = await detectFoundryPackage(prepared.workDir);
    onProgress?.({
      stage: 'copy',
      percent: 36,
      detail: manifest.kind === 'world' ? 'Чтение мира…' : 'Чтение модуля…',
    });
    const docs =
      manifest.kind === 'world'
        ? await loadWorldDocuments(manifest.rootDir)
        : await loadModuleDocuments(manifest.rootDir, manifest.packs);

    if (docs.scenes.length === 0 && docs.actors.length === 0) {
      throw new Error(
        'В пакете Foundry не найдено сцен и актёров. Проверьте, что выбран мир/модуль с данными (не пустой шаблон).',
      );
    }

    return { manifest, docs, cleanup: prepared.cleanup };
  } catch (e) {
    await prepared.cleanup();
    throw e;
  }
}
