import path from "node:path";
import type { SupabaseClient } from "@supabase/supabase-js";
import { CoachSourceSchema } from "@/lib/schemas";
import { createEmbeddings, generateTextResponse, getCoachModel } from "@/lib/openai";
import { formatStatusLabel } from "@/lib/types";
import type {
  CoachConversationRecord,
  CoachMessageRecord,
  CoachMessageSource,
  KnowledgeChunkMatch,
  KnowledgeDocumentRecord,
} from "@/lib/types";

const KNOWLEDGE_BUCKET = "coach-knowledge";
const MAX_CHUNK_CHARS = 1400;
const CHUNK_OVERLAP_CHARS = 220;
const DEFAULT_KNOWLEDGE_MATCHES = 6;
const DEFAULT_MIN_SIMILARITY = 0.22;

type JsonObject = Record<string, unknown>;

type CoachConversationRow = {
  id: string;
  title: string;
  message_count: number;
  created_at: string;
  updated_at: string;
  last_message_at: string | null;
};

type CoachMessageRow = {
  id: string;
  conversation_id: string;
  role: "assistant" | "user";
  content: string;
  created_at: string;
  sources: unknown;
};

type KnowledgeDocumentRow = {
  id: string;
  title: string;
  source_label: string | null;
  file_name: string;
  content_type: string;
  file_size: number;
  status: KnowledgeDocumentRecord["status"];
  chunk_count: number;
  content_length: number;
  error_message: string | null;
  created_at: string;
  updated_at: string;
};

type KnowledgeChunkMatchRow = {
  chunk_id: string;
  document_id: string;
  document_title: string;
  source_label: string | null;
  chunk_index: number;
  content: string;
  metadata: unknown;
  similarity: number;
};

type RecentSessionContextRow = {
  scenario_title: string;
  scenario_target_skill: string;
  status: string;
  updated_at: string;
  final_assessment: {
    summary?: string;
    readiness?: string;
    actionPlan?: string[];
  } | null;
};

type ChunkDraft = {
  content: string;
  metadata: JsonObject;
};

function toJsonObject(value: unknown): JsonObject {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return {};
  }

  return value as JsonObject;
}

function toVectorLiteral(vector: number[]) {
  return `[${vector.join(",")}]`;
}

function normalizeWhitespace(value: string) {
  return value.replace(/\s+/g, " ").trim();
}

function inferContentType(fileName: string) {
  const extension = path.extname(fileName).toLowerCase();

  switch (extension) {
    case ".md":
      return "text/markdown";
    case ".pdf":
      return "application/pdf";
    case ".txt":
    default:
      return "text/plain";
  }
}

function coerceSources(value: unknown) {
  const parsed = CoachSourceSchema.array().safeParse(value);
  return parsed.success ? parsed.data : [];
}

function mapCoachConversationRow(row: CoachConversationRow): CoachConversationRecord {
  return {
    id: row.id,
    title: row.title,
    messageCount: row.message_count,
    createdAt: row.created_at,
    updatedAt: row.updated_at,
    lastMessageAt: row.last_message_at,
  };
}

function mapCoachMessageRow(row: CoachMessageRow): CoachMessageRecord {
  return {
    id: row.id,
    conversationId: row.conversation_id,
    role: row.role,
    content: row.content,
    createdAt: row.created_at,
    sources: coerceSources(row.sources),
  };
}

function mapKnowledgeDocumentRow(row: KnowledgeDocumentRow): KnowledgeDocumentRecord {
  return {
    id: row.id,
    title: row.title,
    sourceLabel: row.source_label,
    fileName: row.file_name,
    contentType: row.content_type,
    fileSize: row.file_size,
    status: row.status,
    chunkCount: row.chunk_count,
    contentLength: row.content_length,
    errorMessage: row.error_message,
    createdAt: row.created_at,
    updatedAt: row.updated_at,
  };
}

function sanitizeFileName(fileName: string) {
  return fileName
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9._-]+/g, "-")
    .replace(/-+/g, "-")
    .replace(/^-|-$/g, "") || "document";
}

function fallbackTitleFromFileName(fileName: string) {
  const baseName = path.basename(fileName, path.extname(fileName));
  return normalizeWhitespace(baseName.replace(/[-_]+/g, " ")) || "Document";
}

function truncate(value: string, maxLength: number) {
  if (value.length <= maxLength) {
    return value;
  }

  return `${value.slice(0, maxLength - 1).trimEnd()}…`;
}

export function createCoachConversationTitle(seed?: string) {
  const normalized = normalizeWhitespace(seed ?? "");
  return normalized.length > 0 ? truncate(normalized, 96) : "Nouvelle conversation";
}

export async function getCoachConversations(
  supabase: SupabaseClient,
  userId: string,
  companyMemberId: string,
) {
  const { data, error } = await supabase
    .from("coach_conversations")
    .select("id, title, message_count, created_at, updated_at, last_message_at")
    .eq("user_id", userId)
    .eq("company_member_id", companyMemberId)
    .order("last_message_at", { ascending: false, nullsFirst: false })
    .order("updated_at", { ascending: false });

  if (error) {
    throw error;
  }

  return ((data ?? []) as CoachConversationRow[]).map(mapCoachConversationRow);
}

export async function getOwnedCoachConversation(
  supabase: SupabaseClient,
  conversationId: string,
  userId: string,
  companyMemberId: string,
) {
  const { data, error } = await supabase
    .from("coach_conversations")
    .select("id, title, message_count, created_at, updated_at, last_message_at")
    .eq("id", conversationId)
    .eq("user_id", userId)
    .eq("company_member_id", companyMemberId)
    .maybeSingle();

  if (error) {
    throw error;
  }

  return data ? mapCoachConversationRow(data as CoachConversationRow) : null;
}

export async function getCoachMessages(
  supabase: SupabaseClient,
  conversationId: string,
) {
  const { data, error } = await supabase
    .from("coach_messages")
    .select("id, conversation_id, role, content, created_at, sources")
    .eq("conversation_id", conversationId)
    .order("created_at", { ascending: true });

  if (error) {
    throw error;
  }

  return ((data ?? []) as CoachMessageRow[]).map(mapCoachMessageRow);
}

export async function createCoachConversation(
  supabase: SupabaseClient,
  input: {
    userId: string;
    companyMemberId: string;
    title?: string;
  },
) {
  const { data, error } = await supabase
    .from("coach_conversations")
    .insert({
      user_id: input.userId,
      company_member_id: input.companyMemberId,
      title: createCoachConversationTitle(input.title),
    })
    .select("id, title, message_count, created_at, updated_at, last_message_at")
    .single();

  if (error) {
    throw error;
  }

  return mapCoachConversationRow(data as CoachConversationRow);
}

export async function listKnowledgeDocuments(supabase: SupabaseClient) {
  const { data, error } = await supabase
    .from("knowledge_documents")
    .select(
      "id, title, source_label, file_name, content_type, file_size, status, chunk_count, content_length, error_message, created_at, updated_at",
    )
    .order("updated_at", { ascending: false });

  if (error) {
    throw error;
  }

  return ((data ?? []) as KnowledgeDocumentRow[]).map(mapKnowledgeDocumentRow);
}

export async function extractTextFromKnowledgeFile(file: File) {
  const extension = path.extname(file.name).toLowerCase();
  const buffer = Buffer.from(await file.arrayBuffer());

  switch (extension) {
    case ".txt":
    case ".md":
      return buffer.toString("utf8");
    case ".pdf": {
      const pdfParseModule = await import("pdf-parse");
      const parser = new pdfParseModule.PDFParse({ data: buffer });

      try {
        const parsed = await parser.getText();
        return parsed.text ?? "";
      } finally {
        await parser.destroy();
      }
    }
    default:
      throw new Error("Format de fichier non supporte. Utilisez pdf, txt ou md.");
  }
}

export function buildKnowledgeChunks(rawText: string) {
  const normalized = rawText
    .replace(/\r\n?/g, "\n")
    .replace(/\u0000/g, "")
    .replace(/[ \t]+\n/g, "\n")
    .trim();

  if (!normalized) {
    return [] as ChunkDraft[];
  }

  const chunks: ChunkDraft[] = [];
  let start = 0;

  while (start < normalized.length) {
    let end = Math.min(start + MAX_CHUNK_CHARS, normalized.length);

    if (end < normalized.length) {
      const lastParagraphBreak = normalized.lastIndexOf("\n\n", end);
      const lastLineBreak = normalized.lastIndexOf("\n", end);
      const lastSentenceBreak = normalized.lastIndexOf(". ", end);
      const candidate = [lastParagraphBreak, lastLineBreak, lastSentenceBreak].find(
        (value) => value > start + 700,
      );

      if (candidate && candidate > start) {
        end = candidate + (candidate === lastSentenceBreak ? 1 : 0);
      }
    }

    const content = normalized.slice(start, end).trim();

    if (content.length > 0) {
      chunks.push({
        content,
        metadata: {
          startOffset: start,
          endOffset: end,
          characterCount: content.length,
        },
      });
    }

    if (end >= normalized.length) {
      break;
    }

    start = Math.max(end - CHUNK_OVERLAP_CHARS, start + 1);
  }

  return chunks;
}

export async function ingestKnowledgeDocument(
  supabase: SupabaseClient,
  input: {
    file: File;
    title?: string;
    sourceLabel?: string;
    userId: string;
    companyMemberId: string | null;
  },
) {
  const fileName = input.file.name.trim();
  const contentType = input.file.type || inferContentType(fileName);
  const title = normalizeWhitespace(input.title ?? "") || fallbackTitleFromFileName(fileName);
  const sourceLabel = normalizeWhitespace(input.sourceLabel ?? "") || null;
  const storagePath = `${new Date().toISOString().slice(0, 10)}/${crypto.randomUUID()}-${sanitizeFileName(fileName)}`;

  const { data: insertedDocument, error: insertError } = await supabase
    .from("knowledge_documents")
    .insert({
      title,
      source_label: sourceLabel,
      file_name: fileName,
      content_type: contentType,
      file_size: input.file.size,
      storage_bucket: KNOWLEDGE_BUCKET,
      storage_path: storagePath,
      status: "processing",
      uploaded_by_user_id: input.userId,
      uploaded_by_member_id: input.companyMemberId,
    })
    .select(
      "id, title, source_label, file_name, content_type, file_size, status, chunk_count, content_length, error_message, created_at, updated_at",
    )
    .single();

  if (insertError) {
    throw insertError;
  }

  const documentId = (insertedDocument as KnowledgeDocumentRow).id;

  try {
    const uploadBuffer = Buffer.from(await input.file.arrayBuffer());
    const { error: uploadError } = await supabase.storage
      .from(KNOWLEDGE_BUCKET)
      .upload(storagePath, uploadBuffer, {
        contentType,
        upsert: false,
      });

    if (uploadError) {
      throw uploadError;
    }

    const extractedText = await extractTextFromKnowledgeFile(input.file);
    const chunks = buildKnowledgeChunks(extractedText);

    if (chunks.length === 0) {
      throw new Error("Le document ne contient pas assez de texte exploitable.");
    }

    const embeddings = await createEmbeddings(chunks.map((chunk) => chunk.content));

    const { error: deleteChunksError } = await supabase
      .from("knowledge_document_chunks")
      .delete()
      .eq("document_id", documentId);

    if (deleteChunksError) {
      throw deleteChunksError;
    }

    const { error: insertChunksError } = await supabase
      .from("knowledge_document_chunks")
      .insert(
        chunks.map((chunk, index) => {
          const embedding = embeddings[index];

          if (!embedding || embedding.length === 0) {
            throw new Error("Une embedding de chunk est manquante.");
          }

          return {
            document_id: documentId,
            chunk_index: index,
            content: chunk.content,
            metadata: chunk.metadata,
            embedding: toVectorLiteral(embedding),
          };
        }),
      );

    if (insertChunksError) {
      throw insertChunksError;
    }

    const { data: updatedDocument, error: updateError } = await supabase
      .from("knowledge_documents")
      .update({
        status: "ready",
        chunk_count: chunks.length,
        content_length: extractedText.length,
        error_message: null,
      })
      .eq("id", documentId)
      .select(
        "id, title, source_label, file_name, content_type, file_size, status, chunk_count, content_length, error_message, created_at, updated_at",
      )
      .single();

    if (updateError) {
      throw updateError;
    }

    return mapKnowledgeDocumentRow(updatedDocument as KnowledgeDocumentRow);
  } catch (error) {
    const message =
      error instanceof Error ? truncate(error.message, 500) : "L'ingestion du document a echoue.";

    await supabase
      .from("knowledge_documents")
      .update({
        status: "failed",
        error_message: message,
      })
      .eq("id", documentId);

    throw error;
  }
}

export async function searchKnowledgeBase(
  supabase: SupabaseClient,
  query: string,
  options?: {
    limit?: number;
    minSimilarity?: number;
  },
) {
  const normalizedQuery = normalizeWhitespace(query);

  if (!normalizedQuery) {
    return [] as KnowledgeChunkMatch[];
  }

  const [embedding] = await createEmbeddings(normalizedQuery);
  if (!embedding || embedding.length === 0) {
    throw new Error("Impossible de calculer l'embedding de la requete.");
  }

  const { data, error } = await supabase.rpc("match_knowledge_document_chunks", {
    query_embedding: toVectorLiteral(embedding),
    match_count: options?.limit ?? DEFAULT_KNOWLEDGE_MATCHES,
    min_similarity: options?.minSimilarity ?? DEFAULT_MIN_SIMILARITY,
  });

  if (error) {
    throw error;
  }

  return ((data ?? []) as KnowledgeChunkMatchRow[]).map((row) => ({
    chunkId: row.chunk_id,
    documentId: row.document_id,
    documentTitle: row.document_title,
    sourceLabel: row.source_label,
    chunkIndex: row.chunk_index,
    content: row.content,
    similarity: row.similarity,
    metadata: toJsonObject(row.metadata),
  }));
}

export function buildRecentSessionContext(rows: RecentSessionContextRow[]) {
  if (rows.length === 0) {
    return {
      text: "",
      sources: [] as CoachMessageSource[],
    };
  }

  const text = rows
    .map((row, index) => {
      const actionPlan = Array.isArray(row.final_assessment?.actionPlan)
        ? row.final_assessment?.actionPlan.slice(0, 3).join(" | ")
        : "";

      return [
        `Simulation ${index + 1}`,
        `Scenario: ${row.scenario_title}`,
        `Competence ciblee: ${row.scenario_target_skill}`,
        `Statut: ${formatStatusLabel(row.status as never)}`,
        `Mise a jour: ${new Date(row.updated_at).toISOString()}`,
        row.final_assessment?.summary
          ? `Synthese: ${row.final_assessment.summary}`
          : "Synthese: indisponible",
        row.final_assessment?.readiness
          ? `Readiness: ${row.final_assessment.readiness}`
          : "",
        actionPlan ? `Pistes d'action: ${actionPlan}` : "",
      ]
        .filter(Boolean)
        .join("\n");
    })
    .join("\n\n");

  const sources = rows.map((row, index) => ({
    id: `report-${index + 1}`,
    kind: "report" as const,
    label: row.scenario_title,
    detail: truncate(
      normalizeWhitespace(
        row.final_assessment?.summary ??
          `Dernier statut connu: ${formatStatusLabel(row.status as never)}.`,
      ),
      220,
    ),
    similarity: null,
    documentId: null,
    chunkId: null,
  }));

  return { text, sources };
}

export function buildProfileContextSources(input: {
  onboardingPromptContext: string;
  actionPlanPrompt: string;
}) {
  const sources: CoachMessageSource[] = [];

  if (input.onboardingPromptContext.trim()) {
    sources.push({
      id: "profile-onboarding",
      kind: "profile",
      label: "Onboarding utilisateur",
      detail: truncate(normalizeWhitespace(input.onboardingPromptContext), 220),
      similarity: null,
      documentId: null,
      chunkId: null,
    });
  }

  if (input.actionPlanPrompt.trim()) {
    sources.push({
      id: "profile-action-plan",
      kind: "profile",
      label: "Plan d'action utilisateur",
      detail: truncate(normalizeWhitespace(input.actionPlanPrompt), 220),
      similarity: null,
      documentId: null,
      chunkId: null,
    });
  }

  return sources;
}

export function buildKnowledgeSources(matches: KnowledgeChunkMatch[]) {
  return matches.map((match) => ({
    id: `knowledge-${match.chunkId}`,
    kind: "knowledge" as const,
    label: match.documentTitle,
    detail: truncate(
      normalizeWhitespace(
        match.sourceLabel
          ? `${match.sourceLabel} - ${match.content}`
          : `Extrait ${match.chunkIndex + 1} - ${match.content}`,
      ),
      260,
    ),
    similarity: match.similarity,
    documentId: match.documentId,
    chunkId: match.chunkId,
  }));
}

export function buildKnowledgeGapSource() {
  return {
    id: "knowledge-gap",
    kind: "system" as const,
    label: "Base documentaire",
    detail:
      "Aucune source suffisamment proche n'a ete retrouvee dans la base documentaire globale pour appuyer une reponse detaillee.",
    similarity: null,
    documentId: null,
    chunkId: null,
  };
}

export function buildCoachPrompt(input: {
  userMessage: string;
  conversationHistory: CoachMessageRecord[];
  onboardingPromptContext: string;
  actionPlanPrompt: string;
  recentSessionContext: string;
  knowledgeMatches: KnowledgeChunkMatch[];
}) {
  const historyBlock =
    input.conversationHistory.length > 0
      ? input.conversationHistory
          .slice(-12)
          .map((message) =>
            `${message.role === "user" ? "Utilisateur" : "Coach"}: ${message.content}`,
          )
          .join("\n\n")
      : "Aucun message precedent.";

  const knowledgeBlock =
    input.knowledgeMatches.length > 0
      ? input.knowledgeMatches
          .map((match, index) =>
            [
              `Source ${index + 1}`,
              `Titre: ${match.documentTitle}`,
              `Repere: ${match.sourceLabel ?? `Extrait ${match.chunkIndex + 1}`}`,
              `Similarite: ${match.similarity.toFixed(3)}`,
              `Contenu: ${match.content}`,
            ].join("\n"),
          )
          .join("\n\n")
      : "Aucune source suffisamment proche n'a ete retrouvee dans la base documentaire.";

  return [
    "CONTEXTE UTILISATEUR",
    input.onboardingPromptContext.trim() || "Aucun onboarding renseigne.",
    "",
    "PLAN D'ACTION",
    input.actionPlanPrompt.trim() || "Aucun plan d'action renseigne.",
    "",
    "HISTORIQUE DE SIMULATIONS",
    input.recentSessionContext.trim() || "Aucun historique de simulations exploitable.",
    "",
    "HISTORIQUE DE CONVERSATION",
    historyBlock,
    "",
    "QUESTION ACTUELLE",
    input.userMessage.trim(),
    "",
    "BASE DOCUMENTAIRE",
    knowledgeBlock,
  ].join("\n");
}

export function buildCoachInstructions(options: {
  hasKnowledgeCoverage: boolean;
}) {
  return [
    "Tu es Evoluteam Coach, un coach operationnel francophone.",
    "Tu aides le manager avec des recommandations actionnables, claires et directement exploitables.",
    "Appuie-toi sur le contexte utilisateur, son plan d'action, ses simulations passees et les extraits de base documentaire fournis.",
    "N'invente ni politique interne, ni donnees, ni bonnes pratiques non presentes dans les sources.",
    "Quand la base documentaire est insuffisante, dis-le explicitement et reste prudent au lieu de combler les trous.",
    options.hasKnowledgeCoverage
      ? "Cite implicitement les enseignements clefs tires des sources fournies, sans recopier mot pour mot de longs passages."
      : "La base documentaire est insuffisante sur cette question. Mentionne clairement cette limite dans ta reponse.",
    "Structure ta reponse en texte simple, sans markdown complexe, avec des recommandations concretes, des formulations breves et professionnelles.",
  ].join("\n");
}

export async function generateCoachReply(input: {
  userMessage: string;
  conversationHistory: CoachMessageRecord[];
  onboardingPromptContext: string;
  actionPlanPrompt: string;
  recentSessionContext: string;
  knowledgeMatches: KnowledgeChunkMatch[];
}) {
  const hasKnowledgeCoverage = input.knowledgeMatches.length > 0;

  return generateTextResponse({
    model: getCoachModel(),
    instructions: buildCoachInstructions({ hasKnowledgeCoverage }),
    input: buildCoachPrompt(input),
    temperature: 0.4,
    maxOutputTokens: 700,
  });
}
