import OpenAI from "openai";
import { zodTextFormat } from "openai/helpers/zod";
import { ZodError, type ZodType } from "zod";

const DEFAULT_MODEL = "gpt-5.2";

let client: OpenAI | null = null;

export function getOpenAIClient() {
  const apiKey = process.env.OPENAI_API_KEY;

  if (!apiKey) {
    throw new Error(
      "La variable d'environnement OPENAI_API_KEY est manquante. Ajoutez-la avant d'utiliser la simulation.",
    );
  }

  if (!client) {
    client = new OpenAI({ apiKey });
  }

  return client;
}

type ParseStructuredOptions<T> = {
  schema: ZodType<T>;
  schemaName: string;
  model?: string;
  instructions: string;
  input: string;
  temperature?: number;
  maxOutputTokens?: number;
};

type GenerateTextOptions = {
  model?: string;
  instructions: string;
  input: string;
  temperature?: number;
  maxOutputTokens?: number;
};

type OpenAITextResponse = {
  output_text?: string | null;
  output?: Array<{
    type: string;
    content?: Array<{
      type: string;
      text?: string;
    }>;
  }>;
};

function extractResponseText(response: OpenAITextResponse) {
  const directText = response.output_text?.trim();

  if (directText) {
    return directText;
  }

  const text = (response.output ?? [])
    .flatMap((item) => (item.type === "message" ? item.content ?? [] : []))
    .filter((content) => content.type === "output_text")
    .map((content) => content.text ?? "")
    .join("")
    .trim();

  if (!text) {
    throw new Error("La reponse structuree est vide.");
  }

  return text;
}

function extractJsonCandidate(rawText: string) {
  const trimmed = rawText.trim();
  const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
  const normalized = (fenced?.[1] ?? trimmed).trim();

  const firstObject = normalized.indexOf("{");
  const lastObject = normalized.lastIndexOf("}");

  if (firstObject !== -1 && lastObject > firstObject) {
    return normalized.slice(firstObject, lastObject + 1);
  }

  const firstArray = normalized.indexOf("[");
  const lastArray = normalized.lastIndexOf("]");

  if (firstArray !== -1 && lastArray > firstArray) {
    return normalized.slice(firstArray, lastArray + 1);
  }

  return normalized;
}

function parseStructuredText<T>(schema: ZodType<T>, rawText: string) {
  return schema.parse(JSON.parse(extractJsonCandidate(rawText)));
}

function isStructuredParsingError(error: unknown) {
  if (error instanceof SyntaxError || error instanceof ZodError) {
    return true;
  }

  return error instanceof Error && /json|schema|structured/i.test(error.message);
}

function normalizeStructuredError(error: unknown) {
  if (error instanceof Error) {
    if (error.message.includes("OPENAI_API_KEY")) {
      return error;
    }

    if (!isStructuredParsingError(error)) {
      return error;
    }
  }

  return new Error(
    "Le modele a renvoye une reponse invalide. Reessayez.",
  );
}

export async function parseStructuredResponse<T>({
  schema,
  schemaName,
  model,
  instructions,
  input,
  temperature,
  maxOutputTokens,
}: ParseStructuredOptions<T>) {
  const api = getOpenAIClient();
  const targetModel = model ?? DEFAULT_MODEL;
  const textFormat = zodTextFormat(schema, schemaName);
  let lastError: unknown;
  let lastRawText: string | null = null;

  for (let attempt = 0; attempt < 2; attempt += 1) {
    try {
      const response = await api.responses.create({
        model: targetModel,
        instructions,
        input,
        temperature,
        max_output_tokens: maxOutputTokens,
        text: {
          format: textFormat,
        },
      });

      lastRawText = extractResponseText(response);
      return parseStructuredText(schema, lastRawText);
    } catch (error) {
      lastError = error;
    }
  }

  if (lastRawText) {
    try {
      const repairedResponse = await api.responses.create({
        model: targetModel,
        instructions: `Tu corriges une sortie JSON invalide. Tu renvoies uniquement un JSON valide respectant exactement le schema "${schemaName}". Tu conserves le sens d'origine autant que possible et tu n'ajoutes aucun commentaire.`,
        input: `Erreur de validation: ${
          lastError instanceof Error ? lastError.message : "inconnue"
        }\n\nSortie a corriger:\n${lastRawText}`,
        temperature: 0,
        max_output_tokens: Math.max(maxOutputTokens ?? 600, 600),
        text: {
          format: zodTextFormat(schema, `${schemaName}_repair`),
        },
      });

      return parseStructuredText(schema, extractResponseText(repairedResponse));
    } catch (error) {
      lastError = error;
    }
  }

  console.error("Structured response parsing failed", {
    schemaName,
    model: targetModel,
    error: lastError instanceof Error ? lastError.message : String(lastError),
  });

  throw normalizeStructuredError(lastError);
}

export async function generateTextResponse({
  model,
  instructions,
  input,
  temperature,
  maxOutputTokens,
}: GenerateTextOptions) {
  const api = getOpenAIClient();
  const response = await api.responses.create({
    model: model ?? DEFAULT_MODEL,
    instructions,
    input,
    temperature,
    max_output_tokens: maxOutputTokens,
  });

  const text = response.output_text?.trim();

  if (!text) {
    throw new Error("La reponse texte est vide.");
  }

  return text;
}

export async function createEmbeddings(
  input: string | string[],
  options?: {
    model?: string;
  },
) {
  const api = getOpenAIClient();
  const response = await api.embeddings.create({
    model: options?.model ?? getEmbeddingModel(),
    input,
  });

  return response.data.map((entry) => entry.embedding);
}

export function getRoleplayModel() {
  return process.env.OPENAI_ROLEPLAY_MODEL ?? DEFAULT_MODEL;
}

export function getEvaluationModel() {
  return process.env.OPENAI_EVALUATION_MODEL ?? getRoleplayModel();
}

export function getFinalAssessmentModel() {
  return process.env.OPENAI_FINAL_ASSESSMENT_MODEL ?? getEvaluationModel();
}

export function getCoachModel() {
  return process.env.OPENAI_COACH_MODEL ?? DEFAULT_MODEL;
}

export function getEmbeddingModel() {
  return process.env.OPENAI_EMBEDDING_MODEL ?? "text-embedding-3-small";
}
