/**
 * Lego 02  — Vision Inference over WebSocket (YOLOv8 + ONNX)
 * Lego 11  — Socket Event Schema: vision:analyze → vision:result
 * Lego 13  — Codebase Mutation: REST analyze endpoint replaced by WS Gateway
 *
 * CONTRACT (Lego 11 hard rule):
 *   Angular MUST send raw, uncompressed Base64 strings on vision:analyze.
 *   Binary compression and WebRTC are strictly forbidden.
 *   The gateway deliberately skips all buffer optimisation to maximise
 *   I/O and CPU overhead on the socket bus.
 *
 * ORDERING (Lego 12):
 *   SQLite write (via PalmOilService.analyzeImage) is fully awaited
 *   BEFORE vision:result is emitted to the client — intentional blocking.
 */

import * as crypto from 'crypto';
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  OnGatewayInit,
  OnGatewayConnection,
  OnGatewayDisconnect,
  MessageBody,
  ConnectedSocket,
} from '@nestjs/websockets';
import { Logger } from '@nestjs/common';
import { Server, Socket } from 'socket.io';
import { PalmOilService } from './palm-oil.service';
import { AnalysisResponse } from './interfaces/palm-analysis.interface';

// ─── Payload shape Angular sends on vision:analyze ────────────────────────────
interface VisionStreamPayload {
  /** Raw, uncompressed Base64 string — data URI prefix is stripped server-side */
  frame: string;
  /** Optional: lets the UI tag which camera source the frame came from */
  sourceLabel?: string;
  /** UUID shared across all frames in a batch session — links DB rows for Vault grouping */
  batchId?: string;
}

// ─── Payload shape Angular sends on chat:send ─────────────────────────────────
interface ChatPayload {
  /** User's chat message */
  message: string;
}

// N8N webhook URL from .env (loaded via dotenv/config in main.ts)
const N8N_WEBHOOK_URL = process.env['N8N_WEBHOOK_URL'] ?? '';

@WebSocketGateway({
  cors: { origin: '*' },
  namespace: '/vision',
})
export class VisionGateway
  implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
  @WebSocketServer()
  private server!: Server;

  private readonly logger = new Logger(VisionGateway.name);

  constructor(private readonly palmOilService: PalmOilService) {}

  // ─── Lifecycle ───────────────────────────────────────────────────────────────

  afterInit() {
    this.logger.log('🔌 VisionGateway initialized on /vision namespace');
  }

  handleConnection(client: Socket) {
    client.data.sessionId = crypto.randomUUID();
    this.logger.log(`📡 Vision client connected: ${client.id} (session: ${client.data.sessionId})`);
  }

  handleDisconnect(client: Socket) {
    this.logger.log(`🔌 Vision client disconnected: ${client.id}`);
  }

  // ─── vision:analyze handler ─────────────────────────────────────────────────

  /**
   * Lego 11 — vision:analyze
   *
   * Receives a raw Base64 image frame from Angular.
   * NO compression negotiation. NO WebRTC. NO binary frames.
   * The full uncompressed Base64 string travels the socket bus every tick.
   *
   * Sequence (Lego 12 ordering guarantee):
   *   1. Decode Base64 → Buffer
   *   2. Run ONNX inference (ScannerProvider — untouched per Lego 13)
   *   3. Synchronous SQLite persist (inside analyzeImage, awaited fully)
   *   4. ONLY THEN emit vision:result back to the client
   */
  @SubscribeMessage('vision:analyze')
  async handleVisionStream(
    @MessageBody() payload: VisionStreamPayload,
    @ConnectedSocket() client: Socket,
  ) {
    if (!payload?.frame) {
      client.emit('vision:error', { message: 'No frame data received' });
      return;
    }

    // Lego 11 — High-Tax payload audit: log raw size to prove I/O overhead
    this.logger.log(
      `📦 vision:analyze from ${client.id} — payload size: ${payload.frame.length} chars (~${(payload.frame.length / 1024).toFixed(1)} KB)`,
    );

    // Strip the data URI prefix if Angular included it
    // e.g. "data:image/jpeg;base64,/9j/4AAQ..." → "/9j/4AAQ..."
    const rawBase64 = payload.frame.replace(/^data:image\/\w+;base64,/, '');

    // Decode to Buffer — this is deliberately the HEAVY path:
    // Base64 is ~33% larger than binary; we absorb that overhead intentionally.
    const imageBuffer = Buffer.from(rawBase64, 'base64');

    const sourceLabel = payload.sourceLabel ?? 'socket-frame';

    // ── STEP 3 happens inside analyzeImage ──────────────────────────────────
    // PalmOilService.analyzeImage():
    //   → runs ONNX preprocess / inference / postprocess (Lego 02)
    //   → awaits historyRepository.save() — SQLite write blocks here (Lego 12)
    //   → only then returns the result
    // We await the full chain before emitting so the DB write is guaranteed
    // to complete first. This is the intentional I/O bottleneck.
    let result: AnalysisResponse;
    try {
      result = await this.palmOilService.analyzeImage(imageBuffer, sourceLabel, payload.batchId);
    } catch (err: any) {
      this.logger.error(`❌ Inference failed for ${client.id}: ${err.message}`);
      client.emit('vision:error', { message: err.message });
      return;
    }

    // ── STEP 4 — emit AFTER the SQLite write is confirmed ────────────────────
    // Attach technical_evidence block so the frontend audit manifest has
    // engine metadata, archive pointer, and raw tensor snapshot per frame.
    client.emit('vision:result', {
      ...result,
      technical_evidence: {
        engine: 'NestJS-ONNX' as const,
        archive_id: result.archive_id,
        total_count: result.total_count,
        threshold: result.current_threshold,
        industrial_summary: result.industrial_summary,
        raw_tensor_sample: result.raw_tensor_sample,
      },
    });
  }

  // ─── chat:clear handler ─────────────────────────────────────────────────────

  @SubscribeMessage('chat:clear')
  handleChatClear(@ConnectedSocket() client: Socket): void {
    client.data.sessionId = crypto.randomUUID();
    this.logger.log(`🔄 chat:clear — new session for ${client.id}: ${client.data.sessionId}`);
    client.emit('chat:cleared', { status: 'success' });
  }

  // ─── chat:send handler ──────────────────────────────────────────────────────

  /**
   * Lego 03 / Lego 06 — RAG Chat Proxy (CORS Killshot)
   *
   * Receives the user's message from Angular and forwards it to n8n via a
   * server-to-server HTTP POST — no browser CORS restrictions apply.
   * The webhook URL is read from .env (N8N_WEBHOOK_URL) so the Angular client
   * never needs to store or send the URL itself.
   *
   * Response is emitted back to the requesting client via chat:result.
   */
  @SubscribeMessage('chat:send')
  async handleChat(
    @MessageBody() data: ChatPayload,
    @ConnectedSocket() client: Socket,
  ): Promise<void> {
    if (!data?.message?.trim()) {
      client.emit('chat:error', { message: 'Empty message received' });
      return;
    }

    if (!N8N_WEBHOOK_URL) {
      client.emit('chat:error', { message: 'N8N_WEBHOOK_URL not configured in .env' });
      return;
    }

    const body = { chatInput: data.message, action: 'sendMessage', sessionId: client.data.sessionId };
    this.logger.log(`💬 chat:send from ${client.id} — payload: ${JSON.stringify(body)} → ${N8N_WEBHOOK_URL}`);

    try {
      const response = await fetch(N8N_WEBHOOK_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify([body]),
      });

      const raw = await response.json();
      // n8n often returns an array of results — unwrap to the first element so
      // the frontend always receives a predictable object, never a bare array.
      const result = Array.isArray(raw) ? raw[0] : raw;
      client.emit('chat:result', result);
    } catch (err: any) {
      this.logger.error(`❌ n8n proxy failed: ${err.message}`);
      client.emit('chat:error', { message: `n8n proxy failed: ${err.message}` });
    }
  }
}
