import { Injectable, OnDestroy } from '@angular/core';
import { Observable, Subject, BehaviorSubject } from 'rxjs';

// ── MPOB standard detection classes (indices 0–5) ───────────────────────────
export const MPOB_CLASSES: string[] = [
  'Empty_Bunch',
  'Underripe',
  'Abnormal',
  'Ripe',
  'Unripe',
  'Overripe',
];

export const HEALTH_ALERT_CLASSES: string[] = ['Abnormal', 'Empty_Bunch'];

// ── Domain types ─────────────────────────────────────────────────────────────

export interface DetectionResult {
  bunch_id: number;
  class: string;
  confidence: number;
  is_health_alert: boolean;
  box: [number, number, number, number];
  norm_box?: [number, number, number, number];
}

export interface InferenceFrame {
  frameId: string;
  batchId?: string;
  imageDataUrl: string;
  detections: DetectionResult[];
  inference_ms: number;
  processing_ms: number;
  total_count: number;
  industrial_summary: Record<string, number>;
  source: 'wasm-local' | 'remote' | 'n8n';
}

// ── Preprocessing constants ──────────────────────────────────────────────────

const MODEL_INPUT_SIZE = 640;

@Injectable({ providedIn: 'root' })
export class InferenceService implements OnDestroy {
  /** Emits each completed inference frame to subscribers */
  readonly results$ = new Subject<InferenceFrame>();

  /** Tracks number of frames pending in the processing queue */
  readonly queueDepth$ = new BehaviorSubject<number>(0);

  private worker: Worker | null = null;
  private destroyed$ = new Subject<void>();
  private pendingMap = new Map<string, (frame: InferenceFrame) => void>();

  constructor() {
    this.initWorker();
  }

  /**
   * Submit a file for local WASM inference.
   * Preprocessing runs synchronously on the calling thread; ONNX execution
   * is dispatched to the background worker.
   */
  analyze(file: File, batchId?: string, mode: 'local-onnx' | 'local-tflite' = 'local-onnx'): Observable<InferenceFrame> {
    return new Observable<InferenceFrame>(observer => {
      const frameId = crypto.randomUUID();
      const processingStart = performance.now();

      this.queueDepth$.next(this.queueDepth$.value + 1);

      const reader = new FileReader();
      reader.onload = async () => {
        try {
          const imageDataUrl = reader.result as string;
          const tensor = await this.preprocessImage(imageDataUrl);

          if (this.worker) {
            this.pendingMap.set(frameId, (frame) => {
              this.queueDepth$.next(Math.max(0, this.queueDepth$.value - 1));
              observer.next(frame);
              observer.complete();
              this.results$.next(frame);
            });

            this.worker.postMessage({
              frameId,
              batchId,
              imageDataUrl,
              tensor: tensor.buffer,
              processingStart,
              mode,
            }, [tensor.buffer]);
          } else {
            // Worker unavailable — emit empty frame so callers can handle gracefully
            const frame: InferenceFrame = this.buildEmptyFrame(
              frameId, batchId, imageDataUrl, processingStart
            );
            this.queueDepth$.next(Math.max(0, this.queueDepth$.value - 1));
            observer.next(frame);
            observer.complete();
            this.results$.next(frame);
          }
        } catch (err) {
          this.queueDepth$.next(Math.max(0, this.queueDepth$.value - 1));
          observer.error(err);
        }
      };
      reader.onerror = () => {
        this.queueDepth$.next(Math.max(0, this.queueDepth$.value - 1));
        observer.error(new Error('FileReader failed to read image'));
      };
      reader.readAsDataURL(file);
    });
  }

  /**
   * Decode image, resize to 640×640, strip alpha, normalize [0,1], return CHW Float32Array.
   * Output shape: [1, 3, 640, 640]
   */
  async preprocessImage(imageDataUrl: string): Promise<Float32Array> {
    const img = await this.loadImage(imageDataUrl);

    const canvas = document.createElement('canvas');
    canvas.width = MODEL_INPUT_SIZE;
    canvas.height = MODEL_INPUT_SIZE;
    const ctx = canvas.getContext('2d')!;
    ctx.drawImage(img, 0, 0, MODEL_INPUT_SIZE, MODEL_INPUT_SIZE);

    const { data } = ctx.getImageData(0, 0, MODEL_INPUT_SIZE, MODEL_INPUT_SIZE);
    const pixelCount = MODEL_INPUT_SIZE * MODEL_INPUT_SIZE;
    const tensor = new Float32Array(3 * pixelCount);

    // RGBA → CHW: R channel, then G channel, then B channel
    for (let i = 0; i < pixelCount; i++) {
      tensor[i]                    = data[i * 4]     / 255.0; // R
      tensor[pixelCount + i]       = data[i * 4 + 1] / 255.0; // G
      tensor[2 * pixelCount + i]   = data[i * 4 + 2] / 255.0; // B
    }

    return tensor;
  }

  ngOnDestroy(): void {
    this.destroyed$.next();
    this.destroyed$.complete();
    this.worker?.terminate();
    this.worker = null;
    this.results$.complete();
    this.queueDepth$.complete();
  }

  // ── Private helpers ────────────────────────────────────────────────────────

  private initWorker(): void {
    try {
      // Worker script is expected at assets/workers/inference.worker.js
      // Built separately; graceful degradation if absent
      this.worker = new Worker(
        new URL('../workers/inference.worker', import.meta.url),
        { type: 'module' }
      );

      this.worker.onmessage = ({ data }: MessageEvent<InferenceFrame>) => {
        const resolve = this.pendingMap.get(data.frameId);
        if (resolve) {
          this.pendingMap.delete(data.frameId);
          resolve(data);
        }
      };

      this.worker.onerror = (err) => {
        console.warn('[InferenceService] Worker error — falling back to no-op mode', err);
        this.worker = null;
        // Complete all pending observables so callers never hang
        for (const [frameId, resolve] of this.pendingMap) {
          resolve(this.buildEmptyFrame(frameId, undefined, '', performance.now()));
        }
        this.pendingMap.clear();
      };
    } catch {
      // Worker URL may not exist during initial scaffolding — safe to ignore
      this.worker = null;
    }
  }

  private loadImage(dataUrl: string): Promise<HTMLImageElement> {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = reject;
      img.src = dataUrl;
    });
  }

  private buildEmptyFrame(
    frameId: string,
    batchId: string | undefined,
    imageDataUrl: string,
    processingStart: number,
  ): InferenceFrame {
    return {
      frameId,
      batchId,
      imageDataUrl,
      detections: [],
      inference_ms: 0,
      processing_ms: performance.now() - processingStart,
      total_count: 0,
      industrial_summary: {},
      source: 'wasm-local',
    };
  }
}
