import {
  AfterViewInit,
  Component,
  ElementRef,
  OnDestroy,
  OnInit,
  ViewChild,
} from '@angular/core';
import { AsyncPipe, DecimalPipe, KeyValuePipe, NgClass, NgStyle } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Select, Store } from '@ngxs/store';
import { Observable, Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { VisionState } from '../store/vision.state';
import { SubmitBatchAnalysis } from '../store/vision.actions';
import { InferenceFrame } from '../services/inference.service';

type EngineMode = 'local-onnx' | 'local-tflite' | 'remote';

@Component({
  selector: 'app-analyzer',
  standalone: true,
  imports: [
    AsyncPipe,
    DecimalPipe,
    KeyValuePipe,
    NgClass,
    NgStyle,
    FormsModule,
    MatSelectModule,
    MatFormFieldModule,
    MatButtonModule,
    MatProgressSpinnerModule,
    MatCardModule,
    MatIconModule,
  ],
  templateUrl: './analyzer.component.html',
  styleUrl: './analyzer.component.scss',
})
export class AnalyzerComponent implements OnInit, AfterViewInit, OnDestroy {
  @Select(VisionState.loading) loading$!: Observable<boolean>;

  @ViewChild('resultCanvas') resultCanvasRef!: ElementRef<HTMLCanvasElement>;
  @ViewChild('videoEl') videoElRef!: ElementRef<HTMLVideoElement>;

  mode: EngineMode = 'remote';
  isViewInitialized = false;
  inputMode: 'file' | 'camera' = 'file';
  isDragOver = false;
  isCameraActive = false;
  currentFrame: InferenceFrame | null = null;
  batchFrames: InferenceFrame[] = [];
  selectedFrameIndex = 0;

  readonly engineOptions: { value: EngineMode; label: string; icon: string }[] = [
    { value: 'local-onnx', label: 'Local — ONNX WASM', icon: 'memory' },
    { value: 'local-tflite', label: 'Local — TFLite WASM', icon: 'layers' },
    { value: 'remote', label: 'Remote — NestJS Server', icon: 'cloud' },
  ];

  private mediaStream: MediaStream | null = null;
  private inferenceSub!: Subscription;
  private batchSub!: Subscription;

  constructor(private store: Store) {}

  ngOnInit(): void {
    this.inferenceSub = (this.store.select(VisionState.currentInference) as Observable<InferenceFrame | null>)
      .pipe(filter((f): f is InferenceFrame => !!f))
      .subscribe(frame => {
        this.currentFrame = frame;
        if (this.isViewInitialized) {
          this.renderPredictionsWithBoxes(frame);
        }
      });

    this.batchSub = (this.store.select((state: any) => state.visionState?.batchFrames ?? []) as Observable<InferenceFrame[]>)
      .subscribe(frames => {
        this.batchFrames = frames;
        this.selectedFrameIndex = 0;
      });
  }

  ngAfterViewInit(): void {
    this.isViewInitialized = true;
    if (this.currentFrame) {
      this.renderPredictionsWithBoxes(this.currentFrame);
    }
  }

  ngOnDestroy(): void {
    this.stopCamera();
    this.inferenceSub?.unsubscribe();
    this.batchSub?.unsubscribe();
  }

  // ── Input mode ────────────────────────────────────────────────────────────

  switchInputMode(m: 'file' | 'camera'): void {
    if (m === 'file') this.stopCamera();
    this.inputMode = m;
  }

  // ── File drop ─────────────────────────────────────────────────────────────

  onFileInput(event: Event): void {
    const input = event.target as HTMLInputElement;
    if (input.files?.length) {
      this.submit(Array.from(input.files));
      input.value = '';
    }
  }

  onDrop(event: DragEvent): void {
    event.preventDefault();
    this.isDragOver = false;
    const files = Array.from(event.dataTransfer?.files ?? []).filter(f =>
      f.type.startsWith('image/'),
    );
    if (files.length) this.submit(files);
  }

  onDragOver(event: DragEvent): void {
    event.preventDefault();
    this.isDragOver = true;
  }

  onDragLeave(): void {
    this.isDragOver = false;
  }

  // ── Camera ────────────────────────────────────────────────────────────────

  async startCamera(): Promise<void> {
    try {
      this.mediaStream = await navigator.mediaDevices.getUserMedia({
        video: { facingMode: 'environment', width: 640, height: 640 },
      });
      this.isCameraActive = true;
      const video = this.videoElRef?.nativeElement;
      if (video) video.srcObject = this.mediaStream;
    } catch (err) {
      console.error('[Analyzer] Camera access denied:', err);
    }
  }

  stopCamera(): void {
    this.mediaStream?.getTracks().forEach(t => t.stop());
    this.mediaStream = null;
    this.isCameraActive = false;
    if (this.videoElRef?.nativeElement) {
      this.videoElRef.nativeElement.srcObject = null;
    }
  }

  captureWebcamFrame(): void {
    const video = this.videoElRef?.nativeElement;
    if (!video) return;
    const offscreen = document.createElement('canvas');
    offscreen.width = video.videoWidth || 640;
    offscreen.height = video.videoHeight || 640;
    offscreen.getContext('2d')!.drawImage(video, 0, 0);
    offscreen.toBlob(blob => {
      if (!blob) return;
      this.submit([new File([blob], `webcam-${Date.now()}.jpg`, { type: 'image/jpeg' })]);
    }, 'image/jpeg');
  }

  // ── Canvas renderer ───────────────────────────────────────────────────────

  renderPredictionsWithBoxes(frame: InferenceFrame): void {
    setTimeout(() => {
      const canvas = this.resultCanvasRef?.nativeElement;
      if (!canvas || !frame.imageDataUrl) return;

      const img = new Image();
      img.onload = () => {
        const container = this.resultCanvasRef.nativeElement.parentElement;
        const targetWidth = container?.clientWidth || img.naturalWidth;
        const targetHeight = container?.clientHeight || img.naturalHeight;
        const scale = Math.min(targetWidth / img.naturalWidth, targetHeight / img.naturalHeight);

        canvas.width = img.naturalWidth * scale;
        canvas.height = img.naturalHeight * scale;
        const ctx = canvas.getContext('2d')!;
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

        for (const det of frame.detections) {
          let x: number, y: number, w: number, h: number;
          if (det.norm_box) {
            const [nx1, ny1, nx2, ny2] = det.norm_box;
            x = nx1 * canvas.width;
            y = ny1 * canvas.height;
            w = (nx2 - nx1) * canvas.width;
            h = (ny2 - ny1) * canvas.height;
          } else if (det.box) {
            const [x1, y1, x2, y2] = det.box;
            x = x1 * scale;
            y = y1 * scale;
            w = (x2 - x1) * scale;
            h = (y2 - y1) * scale;
          } else {
            continue;
          }
          const color = this.gradeColor(det.class);

          ctx.strokeStyle = color;
          ctx.lineWidth = 2;
          ctx.strokeRect(x, y, w, h);

          const label = `${det.class} ${(det.confidence * 100).toFixed(1)}%`;
          ctx.font = 'bold 12px sans-serif';
          const tw = ctx.measureText(label).width;
          ctx.fillStyle = color;
          ctx.fillRect(x, y - 20, tw + 8, 20);
          ctx.fillStyle = '#fff';
          ctx.fillText(label, x + 4, y - 5);
        }
      };
      img.src = frame.imageDataUrl;
    }, 0);
  }

  // ── Helpers ───────────────────────────────────────────────────────────────

  confidencePercent(value: number): string {
    return (value * 100).toFixed(1) + '%';
  }

  gradeColor(cls: string): string {
    const palette: Record<string, string> = {
      Ripe: '#4caf50',
      Unripe: '#ff9800',
      Underripe: '#ffeb3b',
      Overripe: '#9c27b0',
      Abnormal: '#f44336',
      Empty_Bunch: '#607d8b',
    };
    return palette[cls] ?? '#757575';
  }

  selectFrame(index: number): void {
    this.selectedFrameIndex = index;
    this.currentFrame = this.batchFrames[index];
    if (this.isViewInitialized) {
      this.renderPredictionsWithBoxes(this.batchFrames[index]);
    }
  }

  prevFrame(): void {
    const next = this.selectedFrameIndex > 0
      ? this.selectedFrameIndex - 1
      : this.batchFrames.length - 1;
    this.selectFrame(next);
  }

  nextFrame(): void {
    const next = this.selectedFrameIndex < this.batchFrames.length - 1
      ? this.selectedFrameIndex + 1
      : 0;
    this.selectFrame(next);
  }

  private submit(files: File[]): void {
    this.store.dispatch(new SubmitBatchAnalysis({ files, mode: this.mode }));
  }
}
