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

import {
  assignPlayerToTeam,
  createPlayerTeamDraft,
  deletePlayerTeam,
  normalizeAppPlayerTeams,
  uniquePlayerTeamName,
} from '../../shared/players/playerTeams';
import type {
  AppPlayer,
  AppPlayerTeam,
  PlayerId,
  PlayerImageOffset,
  PlayersUpsertProgressEvent,
  PlayerTeamId,
} from '../../shared/types/appPlayers';
import {
  clampPlayerImageOffset,
  clampPlayerImageScale,
  DEFAULT_PLAYER_IMAGE_OFFSET,
  DEFAULT_PLAYER_IMAGE_SCALE,
  DEFAULT_PLAYER_RING_COLOR,
  normalizeAppPlayer,
} from '../../shared/types/appPlayers';
import { asPlayerId } from '../../shared/types/ids';
import { normalizeHexColor } from '../../shared/npcs/npcGroups';
import { optimizeImageBufferVisuallyLossless } from '../project/optimizeImageImport.lib.mjs';

type PlayersManifest = {
  players: AppPlayer[];
  teams: AppPlayerTeam[];
};

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 || 'player';
}

function randomPlayerId(): PlayerId {
  return asPlayerId(`player_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`);
}

export class PlayersStore {
  private readonly rootDir: string;
  private readonly filesDir: string;
  private readonly manifestPath: string;
  private players: AppPlayer[] = [];
  private teams: AppPlayerTeam[] = [];
  private loaded = false;

  constructor(userData: string) {
    this.rootDir = path.join(userData, 'players');
    this.filesDir = path.join(this.rootDir, 'files');
    this.manifestPath = path.join(this.rootDir, 'players.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 PlayersManifest;
      this.teams = normalizeAppPlayerTeams(parsed.teams);
      const teamIds = new Set(this.teams.map((t) => t.id));
      this.players = (Array.isArray(parsed.players) ? parsed.players : [])
        .map((p) => normalizeAppPlayer(p, teamIds))
        .filter((p): p is AppPlayer => Boolean(p));
    } catch {
      this.players = [];
      this.teams = [];
      await this.persist();
    }
    this.loaded = true;
  }

  listPlayers(): AppPlayer[] {
    return [...this.players];
  }

  listTeams(): AppPlayerTeam[] {
    return [...this.teams];
  }

  getById(id: PlayerId): AppPlayer | null {
    return this.players.find((p) => p.id === id) ?? null;
  }

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

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

  async upsert(
    input: {
      id?: PlayerId | null;
      name: string;
      filePath?: string | null;
      teamId?: PlayerTeamId | null;
      ringColor?: string;
      imageOffset?: PlayerImageOffset;
      imageScale?: number;
    },
    onProgress?: (p: PlayersUpsertProgressEvent) => void,
  ): Promise<AppPlayer> {
    await this.ensureLoaded();
    const name = input.name.trim();
    if (!name) throw new Error('Player name is required');

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

    const emit = (percent: number, stage: string, detail?: string) => {
      onProgress?.({ percent, stage, ...(detail ? { detail } : {}) });
    };

    emit(5, 'prepare', 'Подготовка…');

    let imageRelPath = existing?.imageRelPath ?? '';
    let sha256 = existing?.sha256 ?? '';
    const id = existing?.id ?? randomPlayerId();

    if (input.filePath) {
      emit(15, 'read', 'Чтение изображения…');
      let buf = await fs.readFile(input.filePath);
      emit(40, 'optimize', 'Оптимизация…');
      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 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 });
      emit(75, 'write', 'Сохранение…');
      await fs.writeFile(abs, buf);
      if (existing && existing.imageRelPath !== imageRelPath) {
        try {
          await fs.unlink(path.join(this.rootDir, existing.imageRelPath));
        } catch {
          /* ignore */
        }
      }
    }

    const teamIds = new Set(this.teams.map((t) => t.id));
    let teamId: PlayerTeamId | null =
      input.teamId !== undefined ? input.teamId : (existing?.teamId ?? null);
    if (teamId && !teamIds.has(teamId)) teamId = null;

    const player: AppPlayer = {
      id,
      name,
      imageRelPath,
      sha256,
      teamId,
      ringColor: normalizeHexColor(
        input.ringColor ?? existing?.ringColor,
        DEFAULT_PLAYER_RING_COLOR,
      ),
      imageOffset: clampPlayerImageOffset(
        input.imageOffset ?? existing?.imageOffset ?? DEFAULT_PLAYER_IMAGE_OFFSET,
      ),
      imageScale: clampPlayerImageScale(
        input.imageScale ?? existing?.imageScale ?? DEFAULT_PLAYER_IMAGE_SCALE,
      ),
    };

    if (existing) {
      this.players = this.players.map((p) => (p.id === player.id ? player : p));
    } else {
      this.players = [...this.players, player];
    }
    emit(95, 'persist', 'Запись…');
    await this.persist();
    emit(100, 'done', 'Готово');
    return player;
  }

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

  async setPlayersOrder(playerIds: PlayerId[]): Promise<AppPlayer[]> {
    await this.ensureLoaded();
    const byId = new Map(this.players.map((p) => [p.id, p]));
    const next: AppPlayer[] = [];
    for (const id of playerIds) {
      const p = byId.get(id);
      if (p) {
        next.push(p);
        byId.delete(id);
      }
    }
    for (const p of byId.values()) next.push(p);
    this.players = next;
    await this.persist();
    return this.listPlayers();
  }

  async upsertTeam(input: {
    id?: PlayerTeamId | null;
    name: string;
    color?: string;
  }): Promise<AppPlayerTeam> {
    await this.ensureLoaded();
    const existing = input.id ? this.teams.find((t) => t.id === input.id) : null;
    if (input.id && !existing) throw new Error('Team not found');
    if (existing) {
      const team: AppPlayerTeam = {
        id: existing.id,
        name: uniquePlayerTeamName(input.name, this.teams, existing.id),
        color: normalizeHexColor(input.color ?? existing.color, DEFAULT_PLAYER_RING_COLOR),
      };
      this.teams = this.teams.map((t) => (t.id === team.id ? team : t));
      await this.persist();
      return team;
    }
    const team = createPlayerTeamDraft(input.name, input.color, this.teams);
    this.teams = [...this.teams, team];
    await this.persist();
    return team;
  }

  async deleteTeam(id: PlayerTeamId): Promise<void> {
    await this.ensureLoaded();
    const next = deletePlayerTeam(this.teams, this.players, id);
    this.teams = next.teams;
    this.players = next.players;
    await this.persist();
  }

  async setTeamsOrder(teamIds: PlayerTeamId[]): Promise<AppPlayerTeam[]> {
    await this.ensureLoaded();
    const byId = new Map(this.teams.map((t) => [t.id, t]));
    const next: AppPlayerTeam[] = [];
    for (const id of teamIds) {
      const t = byId.get(id);
      if (t) {
        next.push(t);
        byId.delete(id);
      }
    }
    for (const t of byId.values()) next.push(t);
    this.teams = next;
    await this.persist();
    return this.listTeams();
  }

  async assignPlayerTeam(playerId: PlayerId, teamId: PlayerTeamId | null): Promise<AppPlayer | null> {
    await this.ensureLoaded();
    const teamIds = new Set(this.teams.map((t) => t.id as string));
    this.players = assignPlayerToTeam(this.players, playerId, teamId, teamIds);
    await this.persist();
    return this.getById(playerId);
  }

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