import { BadRequestException, ForbiddenException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { spawn } from 'child_process';
import { randomUUID } from 'crypto';
import { existsSync } from 'fs';
import { mkdir, readFile, readdir, writeFile } from 'fs/promises';
import { join, resolve } from 'path';
import { AccountProfileService } from '../auth/account_profile.service';
import { CreateSkinGenerationJobDto } from './dto/create_skin_generation_job.dto';
import { SkinGenerationJob, SkinGenerationJobResponse } from './skin_generation.types';

@Injectable()
export class SkinGenerationService {
  private readonly logger = new Logger(SkinGenerationService.name);
  private readonly jobs = new Map<string, SkinGenerationJob>();

  constructor(
    private readonly configService: ConfigService,
    private readonly accountProfileService: AccountProfileService,
  ) {}

  async createJob(userId: bigint, dto: CreateSkinGenerationJobDto): Promise<SkinGenerationJobResponse> {
    const apiKey = this.configService.get<string>('NOVAMAILIO_API_KEY') || process.env.NOVAMAILIO_API_KEY;
    if (!apiKey || apiKey.trim().length === 0) {
      throw new BadRequestException('服务端尚未配置 NOVAMAILIO_API_KEY，无法生成角色皮肤');
    }
    if (!(await this.accountProfileService.canUseRegistrationSkinGeneration(userId))) {
      throw new BadRequestException('该账号没有可用的注册角色生成机会');
    }
    if (await this.accountProfileService.hasRegistrationGeneratedSkin(userId)) {
      throw new BadRequestException('该账号已经使用过注册角色生成机会');
    }
    const activeJob = Array.from(this.jobs.values()).find(
      (job) => job.userId === userId.toString() && (job.status === 'queued' || job.status === 'running'),
    );
    if (activeJob) {
      return this.toResponse(activeJob);
    }

    const jobId = randomUUID();
    const ownerId = userId.toString();
    const now = Date.now();
    const safeName = this.sanitizeName(dto.name || '我的角色');
    const outDir = join(this.getOutputRoot(), ownerId, jobId);
    const sourceImagePath = join(outDir, 'source_character.png');
    await mkdir(outDir, { recursive: true });
    await this.saveSourceImage(dto.source_image_base64, sourceImagePath);
    const job: SkinGenerationJob = {
      jobId,
      userId: ownerId,
      status: 'queued',
      stage: 'queued',
      message: '生成任务已创建，等待开始',
      name: safeName,
      sourceImagePath,
      createdAt: now,
      updatedAt: now,
      outDir,
      statusJsonPath: join(outDir, 'status.json'),
      resultJsonPath: join(outDir, 'result.json'),
    };

    this.jobs.set(jobId, job);
    void this.runJob(job);
    return this.toResponse(job);
  }

  async getJob(userId: bigint, jobId: string): Promise<SkinGenerationJobResponse> {
    const job = this.jobs.get(jobId) || await this.restoreJobFromDisk(jobId);
    if (!job) {
      throw new NotFoundException('皮肤生成任务不存在或已过期');
    }
    if (job.userId !== userId.toString()) {
      throw new ForbiddenException('无权查看该皮肤生成任务');
    }

    await this.refreshJobFromFiles(job);
    return this.toResponse(job);
  }

  private async runJob(job: SkinGenerationJob): Promise<void> {
    job.status = 'running';
    job.stage = 'start';
    job.message = '正在启动服务器角色生成流程';
    job.updatedAt = Date.now();

    try {
      const scriptPath = this.getScriptPath();
      const pythonPath = this.getPythonPath();
      if (!existsSync(scriptPath)) {
        throw new Error(`皮肤生成脚本不存在: ${scriptPath}`);
      }

      const child = spawn(
        pythonPath,
        [
          scriptPath,
          '--source-image',
          job.sourceImagePath,
          '--out-dir',
          job.outDir,
          '--name',
          job.name,
          '--result-json',
          job.resultJsonPath,
          '--status-json',
          job.statusJsonPath,
        ],
        {
          cwd: this.getBackendRoot(),
          env: process.env,
          stdio: ['ignore', 'pipe', 'pipe'],
        },
      );

      child.stdout.on('data', (chunk: Buffer) => this.consumeWorkerOutput(job, chunk));
      child.stderr.on('data', (chunk: Buffer) => this.consumeWorkerOutput(job, chunk));

      const workerExit = await new Promise<{ exitCode: number | null; signal: NodeJS.Signals | null }>(
        (resolveExit, rejectExit) => {
        child.on('error', rejectExit);
          child.on('close', (exitCode, signal) => resolveExit({ exitCode, signal }));
        },
      );

      await this.refreshJobFromFiles(job);
      if (workerExit.signal) {
        throw new Error(`生成进程被中断：${workerExit.signal}`);
      }
      if (workerExit.exitCode !== 0 || job.stage === 'failed' || job.error) {
        throw new Error(job.error || job.message || `生成进程退出码: ${workerExit.exitCode}`);
      }

      job.status = 'completed';
      job.stage = 'done';
      job.message = '新角色皮肤已生成';
      job.updatedAt = Date.now();
      if (job.spritesheetPath) {
        const skinAsset = await this.accountProfileService.saveGeneratedSkinForUser(
          BigInt(job.userId),
          job.spritesheetPath,
          job.name,
          'generated_registration',
        );
        await this.accountProfileService.consumeRegistrationSkinGeneration(BigInt(job.userId));
        job.skinId = skinAsset.skinId;
        job.textureUrl = skinAsset.texture_url;
      }
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      this.logger.error(`皮肤生成失败: ${job.jobId} ${message}`);
      job.status = 'failed';
      job.stage = 'failed';
      job.message = `生成失败：${message}`;
      job.error = message;
      job.updatedAt = Date.now();
    }
  }

  private consumeWorkerOutput(job: SkinGenerationJob, chunk: Buffer): void {
    const line = chunk.toString('utf8').trim();
    if (!line) {
      return;
    }
    job.message = line.split('\n').pop() || job.message;
    job.updatedAt = Date.now();
  }

  private async refreshJobFromFiles(job: SkinGenerationJob): Promise<void> {
    await this.refreshStatusJson(job);
    await this.refreshResultJson(job);
  }

  private async restoreJobFromDisk(jobId: string): Promise<SkinGenerationJob | null> {
    const outputRoot = this.getOutputRoot();
    try {
      const ownerDirs = await readdir(outputRoot, { withFileTypes: true });
      for (const ownerDir of ownerDirs) {
        if (!ownerDir.isDirectory()) {
          continue;
        }
        const outDir = join(outputRoot, ownerDir.name, jobId);
        if (!existsSync(outDir)) {
          continue;
        }
        const now = Date.now();
        const job: SkinGenerationJob = {
          jobId,
          userId: ownerDir.name,
          status: 'running',
          stage: 'restored',
          message: '正在恢复生成任务状态',
          name: 'custom_whale_human',
          sourceImagePath: join(outDir, 'source_character.png'),
          createdAt: now,
          updatedAt: now,
          outDir,
          statusJsonPath: join(outDir, 'status.json'),
          resultJsonPath: join(outDir, 'result.json'),
        };
        await this.refreshJobFromFiles(job);
        this.jobs.set(jobId, job);
        return job;
      }
    } catch {
      return null;
    }
    return null;
  }

  private async refreshStatusJson(job: SkinGenerationJob): Promise<void> {
    const payload = await this.readJson(job.statusJsonPath);
    if (!payload) {
      return;
    }
    job.stage = String(payload.stage || job.stage);
    job.message = String(payload.message || job.message);
    job.updatedAt = Number(payload.updated_at ? Math.floor(payload.updated_at * 1000) : Date.now());
    if (payload.ok === false && job.stage === 'failed') {
      job.status = 'failed';
      job.error = job.message.replace(/^生成失败：/, '');
    }
  }

  private async refreshResultJson(job: SkinGenerationJob): Promise<void> {
    const payload = await this.readJson(job.resultJsonPath);
    if (!payload) {
      return;
    }
    job.logPath = String(payload.log_path || job.logPath || '');
    if (payload.ok === true) {
      job.status = 'completed';
      job.stage = 'done';
      job.message = '新角色皮肤已生成';
      job.spritesheetPath = String(payload.spritesheet_path || '');
      job.reviewPath = String(payload.review_path || '');
      job.feetZoomPath = String(payload.feet_zoom_path || '');
      job.updatedAt = Date.now();
      return;
    }
    if (payload.ok === false && payload.error) {
      job.status = 'failed';
      job.stage = 'failed';
      job.error = String(payload.error);
      job.message = `生成失败：${job.error}`;
      job.updatedAt = Date.now();
    }
  }

  private async readJson(path: string): Promise<Record<string, any> | null> {
    try {
      if (!existsSync(path)) {
        return null;
      }
      const text = await readFile(path, 'utf8');
      if (!text.trim()) {
        return null;
      }
      return JSON.parse(text);
    } catch {
      return null;
    }
  }

  private async toResponse(job: SkinGenerationJob): Promise<SkinGenerationJobResponse> {
    await this.ensureCompletedJobSkinAsset(job);
    const response: SkinGenerationJobResponse = {
      job_id: job.jobId,
      status: job.status,
      stage: job.stage,
      message: job.message,
      name: job.name,
      created_at: job.createdAt,
      updated_at: job.updatedAt,
      hframes: 8,
      vframes: 4,
    };

    if (job.status === 'failed' && job.error) {
      response.error = job.error;
    }

    if (job.status === 'completed' && job.spritesheetPath && existsSync(job.spritesheetPath)) {
      const image = await readFile(job.spritesheetPath);
      response.spritesheet_base64 = image.toString('base64');
      response.mime_type = 'image/png';
      response.skin_id = job.skinId;
      response.texture_url = job.textureUrl;
    }

    return response;
  }

  private async ensureCompletedJobSkinAsset(job: SkinGenerationJob): Promise<void> {
    if (job.status !== 'completed' || !job.spritesheetPath || job.skinId) {
      return;
    }
    if (!existsSync(job.spritesheetPath)) {
      return;
    }
    const skinAsset = await this.accountProfileService.saveGeneratedSkinForUser(
      BigInt(job.userId),
      job.spritesheetPath,
      job.name,
      'generated_registration',
    );
    await this.accountProfileService.consumeRegistrationSkinGeneration(BigInt(job.userId));
    job.skinId = skinAsset.skinId;
    job.textureUrl = skinAsset.texture_url;
  }

  private getOutputRoot(): string {
    return resolve(this.getBackendRoot(), this.configService.get<string>('SKIN_GENERATION_OUTPUT_DIR') || 'generated/skins');
  }

  private getScriptPath(): string {
    return resolve(
      this.getBackendRoot(),
      this.configService.get<string>('SKIN_GENERATION_SCRIPT_PATH') ||
        'scripts/skin_generation/generate_skin_from_prompt.py',
    );
  }

  private getPythonPath(): string {
    return this.configService.get<string>('SKIN_GENERATION_PYTHON') || process.env.PYTHON || 'python3';
  }

  private getBackendRoot(): string {
    return resolve(__dirname, '../../..');
  }

  private async saveSourceImage(base64: string, destinationPath: string): Promise<void> {
    const normalized = base64.trim().replace(/^data:image\/[a-zA-Z0-9.+-]+;base64,/, '');
    let buffer: Buffer;
    try {
      buffer = Buffer.from(normalized, 'base64');
    } catch {
      throw new BadRequestException('角色参考图解析失败');
    }
    if (buffer.length < 512) {
      throw new BadRequestException('角色参考图内容为空或过小');
    }
    if (buffer.length > 8 * 1024 * 1024) {
      throw new BadRequestException('角色参考图不能超过8MB');
    }
    const isPng = buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
    const isJpeg = buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff;
    const isWebp = buffer.length >= 12 && buffer.subarray(0, 4).toString('ascii') === 'RIFF' && buffer.subarray(8, 12).toString('ascii') === 'WEBP';
    if (!isPng && !isJpeg && !isWebp) {
      throw new BadRequestException('角色参考图必须是 PNG、JPG 或 WebP');
    }
    await writeFile(destinationPath, buffer);
  }

  private sanitizeName(value: string): string {
    const normalized = value
      .trim()
      .toLowerCase()
      .replace(/[^a-zA-Z0-9_\u4e00-\u9fa5]+/g, '_')
      .replace(/_+/g, '_')
      .replace(/^_+|_+$/g, '');
    return (normalized || 'custom_whale_human').slice(0, 40);
  }
}
