import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as fs from 'fs';
import * as path from 'path';
import { randomUUID } from 'crypto';
import { AuditReportImage } from '../entities/audit-report-image.entity';
import { ReportBuilderService } from './report-builder.service';

const sharp: any = require('sharp');
export async function uprightImage(buf: Buffer, ext: 'jpg' | 'png'): Promise<Buffer> {
  try {
    const pipeline = sharp(buf, { failOn: 'none' }).rotate(); // rotate() with no argument = apply EXIF orientation
    return ext === 'png'
      ? await pipeline.png().toBuffer()
      : await pipeline.jpeg({ quality: 92 }).toBuffer();
  } catch {
    return buf; // unreadable for sharp: keep the original bytes (detectImage still validated the file)
  }
}
export interface ImageRef { id: number; file: string; caption: string; w: number; h: number; mime: string; src?: string }

const SECTIONS = ['org_brief', 'audit_brief', 'clause'];

/** Reads width / height of a PNG or JPEG straight from the bytes (no extra library). */
export function detectImage(buf: Buffer): { mime: string; ext: string; w: number; h: number } | null {
  if (buf.length > 24 && buf.readUInt32BE(0) === 0x89504e47) {
    return { mime: 'image/png', ext: 'png', w: buf.readUInt32BE(16), h: buf.readUInt32BE(20) };
  }
  if (buf.length > 4 && buf[0] === 0xff && buf[1] === 0xd8) {
    let i = 2;
    while (i + 9 < buf.length) {
      if (buf[i] !== 0xff) { i++; continue; }
      const marker = buf[i + 1];
      if (marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker)) {
        return { mime: 'image/jpeg', ext: 'jpg', h: buf.readUInt16BE(i + 5), w: buf.readUInt16BE(i + 7) };
      }
      i += 2 + buf.readUInt16BE(i + 2);
    }
    return { mime: 'image/jpeg', ext: 'jpg', w: 0, h: 0 };
  }
  return null;
}

@Injectable()
export class ReportImageService {
  constructor(
    @InjectRepository(AuditReportImage, 'scheme_dbs') private readonly repo: Repository<AuditReportImage>,
    private readonly builder: ReportBuilderService,
  ) {}

  /** Folder for the uploaded photos. Override with AUDIT_REPORT_IMAGE_DIR. */
  baseDir(): string {
    return process.env.AUDIT_REPORT_IMAGE_DIR || path.join(process.cwd(), 'storage', 'auditReportImages');
  }

  private maxBytes() { return Number(process.env.AUDIT_REPORT_IMAGE_MAX_MB || 8) * 1024 * 1024; }
  private maxPerReport() { return Number(process.env.AUDIT_REPORT_IMAGE_MAX_COUNT || 60); }

  private view(i: AuditReportImage) {
    return {
      id: i.id, report_id: i.report_id, section_key: i.section_key, clause_id: i.clause_id,
      original_name: i.original_name, mime: i.mime, width: i.width, height: i.height, size_bytes: i.size_bytes,
      caption: i.caption, sort_order: i.sort_order, created_at: i.created_at,
    };
  }

  async list(reportId: number, userId: number, q: { section_key?: string; clause_id?: number } = {}) {
    await this.builder.assertAccess(await this.builder.getReportOrFail(reportId), userId);
    const where: any = { report_id: reportId };
    if (q.section_key) where.section_key = q.section_key;
    if (q.clause_id) where.clause_id = q.clause_id;
    const rows = await this.repo.find({ where, order: { sort_order: 'ASC', id: 'ASC' } });
    return rows.map((r) => this.view(r));
  }

  async upload(
    reportId: number,
    userId: number,
    files: { originalname: string; buffer: Buffer; size: number }[],
    dto: { section_key?: string; clause_id?: number | string; caption?: string },
  ) {
    await this.builder.loadForWrite(reportId, userId); // only while the report is draft / returned
    if (!files?.length) throw new BadRequestException('Choose at least one picture (JPG or PNG).');

    const section = dto.section_key && SECTIONS.includes(dto.section_key) ? dto.section_key : 'org_brief';
    let clauseId: number | null = null;
    if (dto.clause_id !== undefined && dto.clause_id !== null && dto.clause_id !== '' && Number(dto.clause_id) > 0) {
      clauseId = Number(dto.clause_id);
      const [hit] = await this.repo.manager.query('SELECT id FROM audit_report_clauses WHERE id = ? AND report_id = ?', [clauseId, reportId]);
      if (!hit) throw new BadRequestException('That clause row does not belong to this report.');
    }
    const finalSection = clauseId ? 'clause' : section === 'clause' ? 'org_brief' : section;

    const have = await this.repo.count({ where: { report_id: reportId } });
    if (have + files.length > this.maxPerReport()) {
      throw new BadRequestException(`A report can hold up to ${this.maxPerReport()} pictures (it has ${have}).`);
    }

    const dir = path.join(this.baseDir(), String(reportId));
    fs.mkdirSync(dir, { recursive: true });
    const last = await this.repo.find({ where: { report_id: reportId, section_key: finalSection, ...(clauseId ? { clause_id: clauseId } : {}) }, order: { sort_order: 'DESC' }, take: 1 });
    let order = (last[0]?.sort_order ?? 0) + 1;

    const saved: AuditReportImage[] = [];
    for (const f of files) {
      if (f.size > this.maxBytes()) throw new BadRequestException(`"${f.originalname}" is larger than ${this.maxBytes() / 1024 / 1024} MB.`);
      const raw = detectImage(f.buffer);
      if (!raw) throw new BadRequestException(`"${f.originalname}" is not a JPG or PNG picture.`);
      const bytes = await uprightImage(f.buffer, raw.ext as 'jpg' | 'png');
      const info = detectImage(bytes) ?? raw;   // size AFTER rotation: this is what the PDF and Word must use
      const name = `${randomUUID()}.${info.ext}`;
      fs.writeFileSync(path.join(dir, name), bytes);
      saved.push(
        await this.repo.save(this.repo.create({
          report_id: reportId, section_key: finalSection, clause_id: clauseId,
          file_name: `${reportId}/${name}`, original_name: String(f.originalname).slice(0, 250),
          mime: info.mime, width: info.w, height: info.h, size_bytes: bytes.length,
          caption: (dto.caption ?? '').slice(0, 300), sort_order: order++, uploaded_by: userId,
        })),
      );
    }
    return saved.map((s) => this.view(s));
  }

  async update(reportId: number, imageId: number, userId: number, dto: { caption?: string; section_key?: string }) {
    await this.builder.loadForWrite(reportId, userId);
    const img = await this.repo.findOne({ where: { id: imageId, report_id: reportId } });
    if (!img) throw new NotFoundException('Picture not found.');
    if (dto.caption !== undefined) img.caption = String(dto.caption).slice(0, 300);
    if (dto.section_key && SECTIONS.includes(dto.section_key) && !img.clause_id && dto.section_key !== 'clause') img.section_key = dto.section_key;
    return this.view(await this.repo.save(img));
  }

  /** ids = the new order of the pictures (all in the same section / clause). */
  async reorder(reportId: number, userId: number, ids: number[]) {
    await this.builder.loadForWrite(reportId, userId);
    let n = 1;
    for (const id of ids ?? []) {
      await this.repo.update({ id: Number(id), report_id: reportId }, { sort_order: n++ });
    }
    return { ok: true };
  }

    /**
   * One-off repair for pictures uploaded before the orientation fix: rewrites each file upright and corrects the
   * stored width / height. Pictures without an orientation tag are left alone. Safe to run more than once.
   */
  async normalizeStoredPictures(): Promise<{ checked: number; fixed: number }> {
    const rows = await this.repo.find();
    let fixed = 0;
    for (const img of rows) {
      const abs = path.join(this.baseDir(), img.file_name);
      if (!fs.existsSync(abs)) continue;
      const before = fs.readFileSync(abs);
      const hasOrientationTag = before.includes(Buffer.from('Exif\0\0'));
      if (!hasOrientationTag) continue;
      const ext = /png$/i.test(abs) ? 'png' : 'jpg';
      const bytes = await uprightImage(before, ext);
      const info = detectImage(bytes);
      if (!info) continue;
      fs.writeFileSync(abs, bytes);
      img.width = info.w;
      img.height = info.h;
      img.size_bytes = bytes.length;
      await this.repo.save(img);
      fixed += 1;
    }
    return { checked: rows.length, fixed };
  }
  async remove(reportId: number, imageId: number, userId: number) {
    await this.builder.loadForWrite(reportId, userId);
    const img = await this.repo.findOne({ where: { id: imageId, report_id: reportId } });
    if (!img) throw new NotFoundException('Picture not found.');
    await this.repo.delete({ id: imageId });
    try { fs.unlinkSync(path.join(this.baseDir(), img.file_name)); } catch { /* already gone */ }
    return { ok: true };
  }

  /** For the editor thumbnails (any user who can see the report). */
  async fileFor(reportId: number, imageId: number, userId: number): Promise<{ abs: string; mime: string }> {
    await this.builder.assertAccess(await this.builder.getReportOrFail(reportId), userId);
    const img = await this.repo.findOne({ where: { id: imageId, report_id: reportId } });
    if (!img) throw new NotFoundException('Picture not found.');
    const abs = path.join(this.baseDir(), img.file_name);
    if (!fs.existsSync(abs)) throw new NotFoundException('The picture file is missing on the server.');
    return { abs, mime: img.mime };
  }

  // ───────────── used by the PDF / Word renderers ─────────────
  /** Bytes of one picture for the Word export. */
  read(ref: { file: string; mime?: string }): { buffer: Buffer; type: 'jpg' | 'png' } | null {
    try {
      const abs = path.join(this.baseDir(), ref.file);
      if (!fs.existsSync(abs)) return null;
      return { buffer: fs.readFileSync(abs), type: /png$/i.test(abs) ? 'png' : 'jpg' };
    } catch { return null; }
  }

  /** Copy of the view model where every picture reference gets a data: URI for the HTML / PDF. */
  inline(vm: any): any {
    const copy = JSON.parse(JSON.stringify(vm));
    const walk = (node: any) => {
      if (!node || typeof node !== 'object') return;
      if (Array.isArray(node)) { node.forEach(walk); return; }
      if (typeof node.file === 'string' && typeof node.mime === 'string' && 'id' in node) {
        const r = this.read(node);
        if (r) node.src = `data:${node.mime};base64,${r.buffer.toString('base64')}`;
        return;
      }
      Object.values(node).forEach(walk);
    };
    walk(copy.images);
    ['clause_groups', 'perf_groups'].forEach((k) => (copy[k] ?? []).forEach((g: any) => walk(g.rows)));
    ['risk_rows', 'feedback_rows', 'statutory_rows'].forEach((k) => walk(copy[k]));
    return copy;
  }
}
