import { Injectable, Inject } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { SCANNER_TOKEN, IScannerProvider } from './providers/scanner.interface';
import { Jimp } from 'jimp';
import { performance } from 'perf_hooks';
import { AnalysisResponse, IndustrialSummary } from './interfaces/palm-analysis.interface';
import { History } from './entities/history.entity';
import * as fs from 'fs';
import * as path from 'path';

import { MPOB_CLASSES, HEALTH_ALERT_CLASSES } from './constants/mpob-standards';

@Injectable()
export class PalmOilService {
  private readonly ARCHIVE_DIR = path.join(process.cwd(), 'archive');

  constructor(
    @Inject(SCANNER_TOKEN) private readonly scanner: IScannerProvider,
    @InjectRepository(History)
    private readonly historyRepository: Repository<History>,
  ) {
    if (!fs.existsSync(this.ARCHIVE_DIR)) {
      fs.mkdirSync(this.ARCHIVE_DIR, { recursive: true });
    }
  }

  async analyzeImage(imageBuffer: Buffer, originalFilename: string = 'unknown.jpg', batchId?: string): Promise<AnalysisResponse> {
    const processingStart = performance.now();

    // 1. Get original image dimensions
    const imgMeta = await Jimp.read(imageBuffer);
    const originalWidth = imgMeta.width || 640;
    const originalHeight = imgMeta.height || 640;

    // 2. Preprocess
    const tensor = await this.scanner.preprocess(imageBuffer);

    // 3. Inference with timing
    const inferenceStart = performance.now();
    const outputTensor = await this.scanner.inference(tensor);
    const inferenceMs = performance.now() - inferenceStart;

    // 4. Post-process
    const { detections, raw_tensor_sample } = await this.scanner.postprocess(
      outputTensor,
      originalWidth,
      originalHeight,
    );

    // 5. Generate Industrial Summary
    const industrialSummary: IndustrialSummary = {};
    // Initialize with 0 for all known classes
    Object.values(MPOB_CLASSES).forEach((cls) => {
      industrialSummary[cls] = 0;
    });

    detections.forEach((det) => {
      // Re-verify health alert flag just in case
      det.is_health_alert = HEALTH_ALERT_CLASSES.includes(det.class);
      industrialSummary[det.class] = (industrialSummary[det.class] || 0) + 1;
    });

    // console.log('📊 Industrial Summary:', industrialSummary);

    const processingMs = performance.now() - processingStart;
    const archiveId = `palm_${Date.now()}_${Math.floor(Math.random() * 1000)}`;

    const fileName = `${archiveId}.jpg`;
    const fullPath = path.join(this.ARCHIVE_DIR, fileName);

    // Physical Heavy Lifting: Save the image to disk
    fs.writeFileSync(fullPath, imageBuffer);

    // 6. Persistence to SQLite
    const history = new History();
    history.archive_id = archiveId;
    history.filename = originalFilename;
    history.image_path = fullPath; // Store the reference
    history.total_count = detections.length;
    history.industrial_summary = industrialSummary;

    // Ensure detections is an array of objects, not a leaked JSON string
    history.detections = Array.isArray(detections) ? detections : [];

    history.inference_ms = parseFloat(inferenceMs.toFixed(2));
    history.processing_ms = parseFloat(processingMs.toFixed(2));
    if (batchId) history.batch_id = batchId;

    await this.historyRepository.save(history);

    return {
      status: 'success',
      current_threshold: 0.25,
      total_count: detections.length,
      industrial_summary: industrialSummary,
      detections: history.detections,
      image_data: `data:image/jpeg;base64,${imageBuffer.toString('base64')}`,
      inference_ms: history.inference_ms,
      processing_ms: history.processing_ms,
      archive_id: archiveId,
      raw_tensor_sample,
    };
  }

  async getHistory(): Promise<History[]> {
    // Return objects directly; TypeORM simple-json handles parsing
    return this.historyRepository.find({
      order: {
        created_at: 'DESC',
      },
      take: 50,
    });
  }

  async getBatchDetails(batchId: string): Promise<{
    batchId: string;
    frameCount: number;
    totalDetections: number;
    avgInferenceMs: number;
    avgProcessingMs: number;
    batchStart: Date | null;
    batchEnd: Date | null;
    classTally: Record<string, number>;
    frames: History[];
  }> {
    const frames = await this.historyRepository.find({
      where: { batch_id: batchId },
      order: { created_at: 'ASC' },
    });

    if (!frames.length) {
      return { batchId, frameCount: 0, totalDetections: 0, avgInferenceMs: 0, avgProcessingMs: 0, batchStart: null, batchEnd: null, classTally: {}, frames: [] };
    }

    const frameCount = frames.length;
    const totalDetections = frames.reduce((s, f) => s + (f.total_count ?? 0), 0);
    const avgInferenceMs = parseFloat((frames.reduce((s, f) => s + (f.inference_ms ?? 0), 0) / frameCount).toFixed(2));
    const avgProcessingMs = parseFloat((frames.reduce((s, f) => s + (f.processing_ms ?? 0), 0) / frameCount).toFixed(2));

    const classTally: Record<string, number> = {};
    for (const frame of frames) {
      const summary = frame.industrial_summary as Record<string, number>;
      if (summary && typeof summary === 'object') {
        for (const [cls, count] of Object.entries(summary)) {
          classTally[cls] = (classTally[cls] ?? 0) + (count ?? 0);
        }
      }
    }

    return {
      batchId,
      frameCount,
      totalDetections,
      avgInferenceMs,
      avgProcessingMs,
      batchStart: frames[0].created_at,
      batchEnd: frames[frameCount - 1].created_at,
      classTally,
      frames,
    };
  }

  async getRecordByArchiveId(archiveId: string): Promise<History | null> {
    return this.historyRepository.findOne({ where: { archive_id: archiveId } });
  }

  async deleteRecord(archiveId: string): Promise<{ deleted: boolean }> {
    const record = await this.historyRepository.findOne({ where: { archive_id: archiveId } });
    if (!record) return { deleted: false };

    // Remove archived image from disk
    if (record.image_path && fs.existsSync(record.image_path)) {
      fs.unlinkSync(record.image_path);
    }

    await this.historyRepository.delete({ archive_id: archiveId });
    return { deleted: true };
  }

  async clearAllHistory(): Promise<{ deleted: number }> {
    const records = await this.historyRepository.find();

    // Remove every archived image from disk
    for (const record of records) {
      if (record.image_path && fs.existsSync(record.image_path)) {
        fs.unlinkSync(record.image_path);
      }
    }

    await this.historyRepository.clear();
    return { deleted: records.length };
  }

  async saveExternalResult(payload: {
    frame: string;
    filename?: string;
    batchId?: string;
    detections: any[];
    industrial_summary: Record<string, number>;
    inference_ms: number;
    processing_ms?: number;
  }): Promise<History> {
    // Strip data URL prefix variants (e.g. "data:image/jpeg;base64,") before decoding
    const rawBase64 = payload.frame.replace(/^data:image\/\w+;base64,/, '');
    const imageBuffer = Buffer.from(rawBase64, 'base64');

    const archiveId = `palm_edge_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
    const fullPath = path.join(this.ARCHIVE_DIR, `${archiveId}.jpg`);
    fs.writeFileSync(fullPath, imageBuffer);

    const history = new History();
    history.archive_id = archiveId;
    history.filename = payload.filename ?? 'edge-frame';
    history.image_path = fullPath;
    // Guard against null/undefined so zero-detection frames persist cleanly
    history.detections = Array.isArray(payload.detections) ? payload.detections : [];
    history.total_count = history.detections.length;
    history.industrial_summary = payload.industrial_summary ?? {};
    history.inference_ms = parseFloat(payload.inference_ms.toFixed(2));
    history.processing_ms = parseFloat((payload.processing_ms ?? 0).toFixed(2));
    if (payload.batchId) history.batch_id = payload.batchId;

    await this.historyRepository.save(history);
    return history;
  }
}
