/**
 * ADR-024 — FIS Protocol Compliance Integration
 * ADR-024.3 — Full FIS Streaming Protocol Alignment
 *
 * All client↔server interactions are multiplexed through a single
 * `request` event carrying a FisAppMessage envelope (header + data).
 * The routing engine branches on `header.serviceId:header.messageName`
 * and always echoes back a FIS-compliant two-packet response:
 *   1. Data packet  — { id, messageID, ..., complete: false, message: JSON.stringify(payload) }
 *   2. Done packet  — { id, messageID, ..., complete: true }
 *
 * This two-packet protocol satisfies NgxSocketService's streaming contract:
 *   observer.next() fires on complete:false, observer.complete() on complete:true.
 *
 * ORDERING GUARANTEE (unchanged from Lego 12):
 *   PalmOilService.analyzeImage() is fully awaited — SQLite write
 *   completes before any vision response is emitted.
 */

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

// ─── FIS Protocol Envelope ────────────────────────────────────────────────────

interface FisAppMessage {
  header?: {
    messageID?: string;
    serviceId?: string;
    messageName?: string;
    [k: string]: unknown;
  };
  data?: any;
  // Legacy flat-format fallback fields
  messageID?: string;
  serviceId?: string;
  operation?: string;
  payload?: any;
}

interface FisAppResponse {
  id: string;
  messageID: string;
  serviceId: string;
  operation: string;
  complete: boolean;
  message?: string;   // JSON-encoded payload for data packets (complete: false)
  payload?: any;      // Used only for push notifications (surveillance)
  error?: string;
}

// ─── Typed inbound payload shapes ────────────────────────────────────────────

interface PalmVisionPayload {
  frame: string;
  sourceLabel?: string;
  batchId?: string;
}

interface ChatPayload {
  message: string;
}

interface HistoryDeletePayload {
  archiveId: string;
}

// ─── Env ──────────────────────────────────────────────────────────────────────

const N8N_WEBHOOK_URL = process.env['N8N_WEBHOOK_URL'] ?? '';

// ─── Gateway ──────────────────────────────────────────────────────────────────

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

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

  constructor(
    private readonly palmOilService: PalmOilService,
    private readonly surveillanceService: SurveillanceService,
  ) {}

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

  onModuleInit() {
    this.surveillanceService.registerMetricsCallback((metrics: SystemMetrics) => {
      const id = crypto.randomUUID();
      const packet: FisAppResponse = {
        id,
        messageID: id,
        serviceId: 'Surveillance',
        operation: 'metricsUpdate',
        complete: true,
        payload: metrics,
      };
      this.server?.emit('response', packet);
    });
  }

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

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

    const snapshot = this.surveillanceService.getLatestMetrics();
    if (snapshot) {
      const snapId = crypto.randomUUID();
      const packet: FisAppResponse = {
        id: snapId,
        messageID: snapId,
        serviceId: 'Surveillance',
        operation: 'metricsUpdate',
        complete: true,
        payload: snapshot,
      };
      client.emit('response', packet);
    }
  }

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

  // ─── Unified message broker ───────────────────────────────────────────────

  /**
   * Single catch-all handler for all client operations.
   *
   * Accepts both FIS protocol envelope (header/data) and legacy flat format.
   * Routing key: `${serviceId}:${operation}`
   *
   * Supported routes:
   *   PalmVision:analyze   — decode Base64 frame, run ONNX, persist, reply
   *   History:getAll       — fetch last 50 history records
   *   History:delete       — delete one record by archiveId
   *   History:clearAll     — wipe all records and archived images
   *   Chat:send            — proxy message to n8n webhook
   *   Chat:clear           — reset session UUID
   *   PalmHistory:GetImage — stream archived image as Base64 data URL
   */
  @SubscribeMessage('request')
  async handleMessage(
    @MessageBody() rawMsg: any,
    @ConnectedSocket() client: Socket,
  ): Promise<void> {
    // NgxSocketService sends JSON.stringify(FisAppMessage) — parse if string
    const msg: FisAppMessage = typeof rawMsg === 'string' ? JSON.parse(rawMsg) : (rawMsg ?? {});

    // Extract fields from FIS envelope; fall back to legacy flat format
    const header = msg.header ?? {};
    const messageID: string = (header.messageID ?? msg.messageID ?? crypto.randomUUID()) as string;
    const serviceId: string = (header.serviceId ?? msg.serviceId ?? '') as string;
    const operation: string = (header.messageName ?? msg.operation ?? '') as string;
    const payload: any = msg.data ?? msg.payload;

    /**
     * FIS streaming two-packet reply:
     *  1. Data packet  (complete: false) — NgxSocketService calls observer.next()
     *  2. Done packet  (complete: true)  — NgxSocketService calls observer.complete()
     * makeNgxCall in DpService reads `res.message` via JSON.parse, so payload
     * must be serialised into the `message` string field.
     */
    const reply = (data: any): void => {
      client.emit('response', {
        id: messageID, messageID, serviceId, operation,
        complete: false,
        message: JSON.stringify(data),
      } satisfies FisAppResponse);
      client.emit('response', {
        id: messageID, messageID, serviceId, operation,
        complete: true,
      } satisfies FisAppResponse);
    };

    const replyError = (error: string): void => {
      client.emit('response', {
        id: messageID, messageID, serviceId, operation,
        complete: false,
        message: JSON.stringify({ error }),
      } satisfies FisAppResponse);
      client.emit('response', {
        id: messageID, messageID, serviceId, operation,
        complete: true,
      } satisfies FisAppResponse);
    };

    const route = `${serviceId}:${operation}`;
    this.logger.log(`📨 request [${route}] from ${client.id}`);

    try {
      switch (route) {

        // ── PalmVision:analyze ─────────────────────────────────────────────
        case 'PalmVision:analyze': {
          const { frame, sourceLabel, batchId } = (payload ?? {}) as PalmVisionPayload;

          if (!frame) {
            replyError('No frame data received');
            return;
          }

          this.logger.log(
            `📦 PalmVision:analyze from ${client.id} — payload size: ${frame.length} chars (~${(frame.length / 1024).toFixed(1)} KB)`,
          );

          const rawBase64 = frame.replace(/^data:image\/\w+;base64,/, '');
          const imageBuffer = Buffer.from(rawBase64, 'base64');

          const result: AnalysisResponse = await this.palmOilService.analyzeImage(
            imageBuffer,
            sourceLabel ?? 'socket-frame',
            batchId,
          );

          reply({
            ...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,
            },
          });
          break;
        }

        // ── History:getAll ─────────────────────────────────────────────────
        case 'History:getAll': {
          const history = await this.palmOilService.getHistory();
          reply(history);
          break;
        }

        // ── History:delete ─────────────────────────────────────────────────
        case 'History:delete': {
          const { archiveId } = (payload ?? {}) as HistoryDeletePayload;
          const result = await this.palmOilService.deleteRecord(archiveId);
          reply(result);
          break;
        }

        // ── History:clearAll ───────────────────────────────────────────────
        case 'History:clearAll': {
          const result = await this.palmOilService.clearAllHistory();
          reply(result);
          break;
        }

        // ── Chat:send ──────────────────────────────────────────────────────
        case 'Chat:send': {
          const { message } = (payload ?? {}) as ChatPayload;

          if (!message?.trim()) {
            replyError('Empty message received');
            return;
          }

          if (!N8N_WEBHOOK_URL) {
            replyError('N8N_WEBHOOK_URL not configured in .env');
            return;
          }

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

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

          const raw = await response.json();
          const chatResult = Array.isArray(raw) ? raw[0] : raw;
          reply(chatResult);
          break;
        }

        // ── Chat:clear ─────────────────────────────────────────────────────
        case 'Chat:clear': {
          client.data.sessionId = crypto.randomUUID();
          this.logger.log(`🔄 Chat:clear — new session for ${client.id}: ${client.data.sessionId}`);
          reply({ status: 'success' });
          break;
        }

        // ── PalmHistory:GetImage ───────────────────────────────────────────
        case 'PalmHistory:GetImage': {
          const { archiveId } = (payload ?? {}) as HistoryDeletePayload;

          if (!archiveId) {
            replyError('No archiveId provided');
            return;
          }

          const record = await this.palmOilService.getRecordByArchiveId(archiveId);
          if (!record || !record.image_path) {
            replyError(`Image not found for archiveId: ${archiveId}`);
            return;
          }

          if (!fs.existsSync(record.image_path)) {
            replyError(`Image file missing on disk for archiveId: ${archiveId}`);
            return;
          }

          const fileBuffer = fs.readFileSync(record.image_path);
          const dataUrl = `data:image/jpeg;base64,${fileBuffer.toString('base64')}`;

          reply({ archiveId, image_data: dataUrl });
          break;
        }

        // ── Unknown route ──────────────────────────────────────────────────
        default: {
          this.logger.warn(`⚠️  Unknown route: ${route}`);
          replyError(`Unknown route: ${route}`);
        }
      }
    } catch (err: any) {
      this.logger.error(`❌ request error [${route}]: ${err.message}`);
      replyError(err.message);
    }
  }
}
