import { Injectable, Logger, ServiceUnavailableException } from '@nestjs/common';
import type {
  AssistantAnswer,
  AssistantAsk,
  BilanField,
  BilanSuggestionsResponse,
  ReindexResult,
  SuggestBilan,
  TranscriptionResult,
} from '@siop/shared';
import { loadEnv } from '../config/env';

/** Proxy vers `siop2-ai` (ADR-004 §4) : le service IA n'est JAMAIS public —
 *  l'API porte l'auth utilisateur (matrice) et le jeton de service interne.
 *  Il traduit aussi le dialecte interne (français, snake_case) vers le
 *  contrat (@siop/shared) — une seule vérité côté clients. */

interface ExtraitInterne {
  source_type: 'DOCUMENT' | 'WORK_ORDER';
  document_id: string | null;
  work_order_id: string | null;
  titre: string;
  locator: string;
  content: string;
  score: number;
}

const MODES = { extractif: 'EXTRACTIVE', genere: 'GENERATED', refus: 'REFUSAL' } as const;
const CONFIANCES = { FORTE: 'HIGH', MOYENNE: 'MEDIUM' } as const;

@Injectable()
export class AssistantService {
  private readonly journal = new Logger(AssistantService.name);
  private readonly env = loadEnv();

  private async appeler<T>(chemin: string, corps: unknown): Promise<T> {
    let reponse: Response;
    try {
      reponse = await fetch(`${this.env.AI_SERVICE_URL}${chemin}`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Service-Token': this.env.AI_SERVICE_TOKEN,
        },
        body: JSON.stringify(corps),
      });
    } catch {
      this.journal.warn(`Service IA injoignable (${chemin})`);
      throw new ServiceUnavailableException(
        'Assistant indisponible pour le moment — réessayez dans un instant.',
      );
    }
    if (!reponse.ok) {
      this.journal.warn(`Service IA a refusé ${chemin} (${reponse.status})`);
      throw new ServiceUnavailableException(
        'Assistant indisponible pour le moment — réessayez dans un instant.',
      );
    }
    return (await reponse.json()) as T;
  }

  async ask(dto: AssistantAsk): Promise<AssistantAnswer> {
    const brut = await this.appeler<{
      mode: keyof typeof MODES;
      answer: string | null;
      extraits: ExtraitInterne[];
      corpus: { documents: number; bilans: number };
    }>('/internal/ask', { question: dto.question });

    return {
      mode: MODES[brut.mode],
      answer: brut.answer,
      excerpts: brut.extraits.map((e) => ({
        sourceType: e.source_type,
        documentId: e.document_id,
        workOrderId: e.work_order_id,
        title: e.titre,
        locator: e.locator,
        content: e.content,
        score: e.score,
      })),
      corpus: { documents: brut.corpus.documents, reports: brut.corpus.bilans },
    };
  }

  async reindex(): Promise<ReindexResult> {
    const brut = await this.appeler<{
      documents_indexes: number;
      documents_ignores: number;
      bilans_indexes: number;
      extraits: number;
    }>('/internal/reindex', {});
    return {
      documentsIndexed: brut.documents_indexes,
      documentsSkipped: brut.documents_ignores,
      reportsIndexed: brut.bilans_indexes,
      chunks: brut.extraits,
    };
  }

  /** Dictée (R5 D5, opt-in) : l'audio ne transite qu'une fois vers `siop2-ai`
   *  — jamais écrit ni sur disque ni en base ici, transmis tel quel en
   *  multipart. Le service IA l'efface aussitôt transcrit (transcription.py). */
  async transcribe(audio: {
    buffer: Buffer;
    originalName: string;
    contentType: string;
  }): Promise<TranscriptionResult> {
    const form = new FormData();
    form.append(
      'fichier',
      new Blob([new Uint8Array(audio.buffer)], { type: audio.contentType }),
      audio.originalName,
    );
    let reponse: Response;
    try {
      reponse = await fetch(`${this.env.AI_SERVICE_URL}/internal/transcrire`, {
        method: 'POST',
        headers: { 'X-Service-Token': this.env.AI_SERVICE_TOKEN },
        body: form,
      });
    } catch {
      this.journal.warn('Service IA injoignable (/internal/transcrire)');
      throw new ServiceUnavailableException(
        'Dictée indisponible pour le moment — réessayez dans un instant.',
      );
    }
    if (!reponse.ok) {
      this.journal.warn(`Service IA a refusé /internal/transcrire (${reponse.status})`);
      throw new ServiceUnavailableException(
        'Dictée indisponible pour le moment — réessayez dans un instant.',
      );
    }
    const brut = (await reponse.json()) as { texte: string };
    return { text: brut.texte };
  }

  async suggestBilan(dto: SuggestBilan): Promise<BilanSuggestionsResponse> {
    const brut = await this.appeler<{
      suggestions: {
        field: BilanField;
        value_id: string;
        label: string;
        confidence: keyof typeof CONFIANCES;
        similar_reports: number;
        score: number;
      }[];
    }>('/internal/suggest', { description: dto.description });

    return {
      suggestions: brut.suggestions.map((s) => ({
        field: s.field,
        valueId: s.value_id,
        label: s.label,
        confidence: CONFIANCES[s.confidence],
        similarReports: s.similar_reports,
        score: s.score,
      })),
    };
  }
}
