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

import type { AppToken, TokenId } from '../../shared/types';
import { asTokenId } from '../../shared/types/ids';
import { optimizeImageBufferVisuallyLossless } from '../project/optimizeImageImport.lib.mjs';

type TokensManifest = {
  tokens: AppToken[];
};

function mimeFromExt(ext: string): string {
  const e = ext.toLowerCase();
  if (e === '.png') return 'image/png';
  if (e === '.jpg' || e === '.jpeg') return 'image/jpeg';
  if (e === '.webp') return 'image/webp';
  if (e === '.gif') return 'image/gif';
  return 'application/octet-stream';
}

function safeFileBase(name: string): string {
  const base = name.replace(/[^\w.\-]+/gu, '_').slice(0, 48);
  return base || 'token';
}

function randomTokenId(): TokenId {
  return asTokenId(`token_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`);
}

export class TokensStore {
  private readonly rootDir: string;
  private readonly filesDir: string;
  private readonly manifestPath: string;
  private tokens: AppToken[] = [];
  private loaded = false;

  constructor(userData: string) {
    this.rootDir = path.join(userData, 'tokens');
    this.filesDir = path.join(this.rootDir, 'files');
    this.manifestPath = path.join(this.rootDir, 'tokens.json');
  }

  async ensureLoaded(): Promise<void> {
    if (this.loaded) return;
    await fs.mkdir(this.filesDir, { recursive: true });
    try {
      const raw = await fs.readFile(this.manifestPath, 'utf8');
      const parsed = JSON.parse(raw) as TokensManifest;
      this.tokens = Array.isArray(parsed.tokens)
        ? parsed.tokens.filter(
            (t): t is AppToken =>
              Boolean(t) &&
              typeof t.id === 'string' &&
              typeof t.name === 'string' &&
              typeof t.imageRelPath === 'string' &&
              typeof t.sha256 === 'string',
          )
        : [];
    } catch {
      this.tokens = [];
      await this.persist();
    }
    this.loaded = true;
  }

  list(): AppToken[] {
    return [...this.tokens];
  }

  getById(id: TokenId): AppToken | null {
    return this.tokens.find((t) => t.id === id) ?? null;
  }

  findBySha256(sha256: string): AppToken | null {
    return this.tokens.find((t) => t.sha256 === sha256) ?? null;
  }

  getImageReadInfo(id: TokenId): { absPath: string; mime: string } | null {
    const token = this.getById(id);
    if (!token) return null;
    const absPath = path.join(this.rootDir, token.imageRelPath);
    return { absPath, mime: mimeFromExt(path.extname(token.imageRelPath)) };
  }

  getImageUrl(id: TokenId): string | null {
    if (!this.getImageReadInfo(id)) return null;
    return `dnd://token?id=${encodeURIComponent(id)}`;
  }

  absPathForRel(relPath: string): string {
    return path.join(this.rootDir, relPath);
  }

  async upsert(input: { id?: TokenId | null; name: string; filePath?: string | null }): Promise<AppToken> {
    await this.ensureLoaded();
    const name = input.name.trim();
    if (!name) throw new Error('Token name is required');

    const existing = input.id ? this.getById(input.id) : null;
    if (input.id && !existing) throw new Error('Token not found');
    if (!existing && !input.filePath) throw new Error('Token image is required');

    let imageRelPath = existing?.imageRelPath ?? '';
    let sha256 = existing?.sha256 ?? '';

    if (input.filePath) {
      let buf = await fs.readFile(input.filePath);
      try {
        const opt = await optimizeImageBufferVisuallyLossless(buf);
        if (opt.buffer && opt.buffer.length > 0) buf = Buffer.from(opt.buffer);
      } catch {
        /* keep original */
      }
      sha256 = crypto.createHash('sha256').update(buf).digest('hex');
      const id = existing?.id ?? randomTokenId();
      const ext = path.extname(input.filePath) || '.png';
      const fileName = `${id}_${safeFileBase(name)}${ext.toLowerCase()}`;
      imageRelPath = path.join('files', fileName).replace(/\\/gu, '/');
      const abs = path.join(this.rootDir, imageRelPath);
      await fs.mkdir(path.dirname(abs), { recursive: true });
      await fs.writeFile(abs, buf);
      if (existing && existing.imageRelPath !== imageRelPath) {
        try {
          await fs.unlink(path.join(this.rootDir, existing.imageRelPath));
        } catch {
          /* ignore */
        }
      }
      const token: AppToken = {
        id,
        name,
        imageRelPath,
        sha256,
      };
      if (existing) {
        this.tokens = this.tokens.map((t) => (t.id === id ? token : t));
      } else {
        this.tokens = [...this.tokens, token];
      }
      await this.persist();
      return token;
    }

    const token: AppToken = {
      id: existing!.id,
      name,
      imageRelPath,
      sha256,
    };
    this.tokens = this.tokens.map((t) => (t.id === token.id ? token : t));
    await this.persist();
    return token;
  }

  /** Импорт токена из внешнего файла (storyline zip) с заданным id или дедупом по sha256. */
  async importFromFile(input: {
    preferredId: TokenId;
    name: string;
    absFilePath: string;
    sha256?: string;
  }): Promise<{ token: AppToken; remappedFrom: TokenId }> {
    await this.ensureLoaded();
    let buf = await fs.readFile(input.absFilePath);
    const sha256 = input.sha256 ?? crypto.createHash('sha256').update(buf).digest('hex');
    const existingByHash = this.findBySha256(sha256);
    if (existingByHash) {
      return { token: existingByHash, remappedFrom: input.preferredId };
    }
    const existingById = this.getById(input.preferredId);
    const id = existingById ? randomTokenId() : input.preferredId;
    try {
      const opt = await optimizeImageBufferVisuallyLossless(buf);
      if (opt.buffer && opt.buffer.length > 0) buf = Buffer.from(opt.buffer);
    } catch {
      /* keep */
    }
    const ext = path.extname(input.absFilePath) || '.png';
    const fileName = `${id}_${safeFileBase(input.name)}${ext.toLowerCase()}`;
    const imageRelPath = path.join('files', fileName).replace(/\\/gu, '/');
    const abs = path.join(this.rootDir, imageRelPath);
    await fs.mkdir(path.dirname(abs), { recursive: true });
    await fs.writeFile(abs, buf);
    const token: AppToken = {
      id,
      name: input.name.trim() || 'Token',
      imageRelPath,
      sha256: crypto.createHash('sha256').update(buf).digest('hex'),
    };
    this.tokens = [...this.tokens, token];
    await this.persist();
    return { token, remappedFrom: input.preferredId };
  }

  async delete(id: TokenId): Promise<void> {
    await this.ensureLoaded();
    const existing = this.getById(id);
    if (!existing) return;
    this.tokens = this.tokens.filter((t) => t.id !== id);
    await this.persist();
    try {
      await fs.unlink(path.join(this.rootDir, existing.imageRelPath));
    } catch {
      /* ignore */
    }
  }

  /** Упаковать выбранные токены в каталог экспорта (`app-tokens/`). */
  async packForExport(tokenIds: string[], exportRoot: string): Promise<void> {
    await this.ensureLoaded();
    const idSet = new Set(tokenIds);
    const selected = this.tokens.filter((t) => idSet.has(t.id));
    if (selected.length === 0) return;
    const outDir = path.join(exportRoot, 'app-tokens');
    const filesOut = path.join(outDir, 'files');
    await fs.mkdir(filesOut, { recursive: true });
    const packed: AppToken[] = [];
    for (const t of selected) {
      const src = path.join(this.rootDir, t.imageRelPath);
      const base = path.basename(t.imageRelPath);
      const destRel = path.join('files', base).replace(/\\/gu, '/');
      await fs.copyFile(src, path.join(outDir, destRel));
      packed.push({ ...t, imageRelPath: destRel });
    }
    await fs.writeFile(
      path.join(outDir, 'tokens.json'),
      `${JSON.stringify({ tokens: packed }, null, 2)}\n`,
      'utf8',
    );
  }

  /**
   * Импорт из `sourceCache/app-tokens/`.
   * @returns map oldTokenId → newTokenId
   */
  async importFromExportDir(sourceCache: string): Promise<Map<string, string>> {
    await this.ensureLoaded();
    const remap = new Map<string, string>();
    const manifestPath = path.join(sourceCache, 'app-tokens', 'tokens.json');
    let raw: string;
    try {
      raw = await fs.readFile(manifestPath, 'utf8');
    } catch {
      return remap;
    }
    let parsed: TokensManifest;
    try {
      parsed = JSON.parse(raw) as TokensManifest;
    } catch {
      return remap;
    }
    const list = Array.isArray(parsed.tokens) ? parsed.tokens : [];
    for (const t of list) {
      if (!t?.id || !t.imageRelPath) continue;
      const abs = path.join(sourceCache, 'app-tokens', t.imageRelPath);
      const { token, remappedFrom } = await this.importFromFile({
        preferredId: asTokenId(t.id),
        name: typeof t.name === 'string' ? t.name : 'Token',
        absFilePath: abs,
        ...(typeof t.sha256 === 'string' ? { sha256: t.sha256 } : {}),
      });
      remap.set(remappedFrom, token.id);
    }
    return remap;
  }

  private async persist(): Promise<void> {
    await fs.mkdir(this.rootDir, { recursive: true });
    const payload: TokensManifest = { tokens: this.tokens };
    await fs.writeFile(this.manifestPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
  }
}
