import "server-only";

import { z } from "zod";
import type { AiConfidence, AiEnvelope, AiUsage, AiWarning } from "@/lib/ai/types";

const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
const DEFAULT_SMART_MODEL = "gpt-4.1";
const DEFAULT_SMART_FALLBACK_MODEL = "gpt-4.1-mini";
const DEFAULT_TIMEOUT_MS = 75_000;
const DEFAULT_MAX_PROMPT_CHARS = 55_000;
const DEFAULT_RETRIES = 1;

type OpenAiChatCompletionResponse = {
  choices?: Array<{
    message?: {
      content?: string | null;
    };
  }>;
  model?: string;
  usage?: {
    prompt_tokens?: number;
    completion_tokens?: number;
    total_tokens?: number;
  };
  error?: {
    message?: string;
  };
};

type OpenAiCallBaseInput<T> = {
  promptVersion: string;
  systemPrompt: string;
  userPrompt: string;
  outputSchema: z.ZodType<T>;
  model?: string | null;
  fallbackModel?: string | null;
  timeoutMs?: number;
  maxPromptChars?: number;
  retries?: number;
  temperature?: number;
};

type OpenAiJsonSchemaInput<T> = OpenAiCallBaseInput<T> & {
  schemaName: string;
  jsonSchema: Record<string, unknown>;
};

type OpenAiJsonObjectInput<T> = OpenAiCallBaseInput<T> & {
  initialWarnings?: AiWarning[];
};

type OpenAiExecutionResult<T> = {
  data: T;
  model: string | null;
  usage: AiUsage | null;
  rawResponse: Record<string, unknown> | null;
  warnings: AiWarning[];
};

function parsePositiveInteger(value: string | undefined, fallback: number) {
  const parsed = Number.parseInt((value ?? "").trim(), 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}

function getOpenAiApiKey() {
  return process.env.OPENAI_API_KEY?.trim() || process.env.API_KEY?.trim() || process.env.api_key?.trim() || "";
}

function getSmartModel() {
  return process.env.OPENAI_SMART_MODEL?.trim() || DEFAULT_SMART_MODEL;
}

function getSmartFallbackModel() {
  return process.env.OPENAI_SMART_FALLBACK_MODEL?.trim() || DEFAULT_SMART_FALLBACK_MODEL;
}

function getOpenAiBaseUrl() {
  return (process.env.OPENAI_API_BASE_URL?.trim() || DEFAULT_OPENAI_BASE_URL).replace(/\/+$/, "");
}

function truncatePrompt(value: string, maxChars: number) {
  if (value.length <= maxChars) {
    return {
      prompt: value,
      truncated: false,
    };
  }

  return {
    prompt: `${value.slice(0, maxChars)}\n\n[TEXT_TRUNCATED_TO_${maxChars}_CHARS]`,
    truncated: true,
  };
}

function usageFromPayload(payload: OpenAiChatCompletionResponse): AiUsage {
  return {
    promptTokens: payload.usage?.prompt_tokens ?? null,
    completionTokens: payload.usage?.completion_tokens ?? null,
    totalTokens: payload.usage?.total_tokens ?? null,
  };
}

function extractJsonObject(rawContent: string) {
  const trimmed = rawContent.trim();

  try {
    return JSON.parse(trimmed) as unknown;
  } catch {
    // Continue with safe extraction.
  }

  const withoutFence = trimmed
    .replace(/^```json\s*/i, "")
    .replace(/^```\s*/i, "")
    .replace(/\s*```$/i, "")
    .trim();

  try {
    return JSON.parse(withoutFence) as unknown;
  } catch {
    // Continue with bracket extraction.
  }

  const firstBrace = withoutFence.indexOf("{");
  const lastBrace = withoutFence.lastIndexOf("}");

  if (firstBrace >= 0 && lastBrace > firstBrace) {
    return JSON.parse(withoutFence.slice(firstBrace, lastBrace + 1)) as unknown;
  }

  throw new Error("La respuesta de OpenAI no contiene un JSON valido.");
}

function getErrorMessage(error: unknown) {
  if (error instanceof Error) {
    return error.message;
  }

  return String(error);
}

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

  const record = value as Record<string, unknown>;
  const confidence = record.confidence;

  if (confidence === "low" || confidence === "medium" || confidence === "high") {
    return confidence;
  }

  if (typeof confidence === "number") {
    if (confidence >= 0.8) {
      return "high";
    }

    if (confidence >= 0.5) {
      return "medium";
    }

    return "low";
  }

  return null;
}

async function executeOpenAiCall<T>(
  args: {
    promptVersion: string;
    systemPrompt: string;
    userPrompt: string;
    outputSchema: z.ZodType<T>;
    model: string;
    responseFormat: Record<string, unknown>;
    timeoutMs: number;
    retries: number;
    temperature: number;
  },
): Promise<OpenAiExecutionResult<T>> {
  const apiKey = getOpenAiApiKey();

  if (!apiKey) {
    throw new Error("No se encontro API key para OpenAI (OPENAI_API_KEY o api_key).");
  }

  const warnings: AiWarning[] = [];
  const baseUrl = getOpenAiBaseUrl();
  const requestBody = {
    model: args.model,
    temperature: args.temperature,
    response_format: args.responseFormat,
    messages: [
      { role: "system", content: args.systemPrompt },
      { role: "user", content: args.userPrompt },
    ],
  };

  let lastError: unknown = null;

  for (let attempt = 0; attempt <= args.retries; attempt += 1) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), args.timeoutMs);

    try {
      const response = await fetch(`${baseUrl}/chat/completions`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${apiKey}`,
        },
        body: JSON.stringify(requestBody),
        signal: controller.signal,
      });

      const payload = (await response.json().catch(() => ({}))) as OpenAiChatCompletionResponse;

      if (!response.ok) {
        const apiMessage = payload.error?.message ? ` ${payload.error.message}` : "";
        const retryable = response.status >= 500 || response.status === 429;

        if (retryable && attempt < args.retries) {
          warnings.push({
            code: "retrying_openai_request",
            message: `OpenAI devolvio ${response.status}. Reintentando (${attempt + 1}/${args.retries + 1}).`,
          });
          continue;
        }

        throw new Error(`OpenAI devolvio ${response.status}.${apiMessage}`);
      }

      const rawContent = payload.choices?.[0]?.message?.content;

      if (!rawContent || typeof rawContent !== "string") {
        throw new Error("OpenAI no devolvio contenido util.");
      }

      const parsedJson = extractJsonObject(rawContent);
      const parsed = args.outputSchema.safeParse(parsedJson);

      if (!parsed.success) {
        throw new Error(`La salida de OpenAI no cumple el esquema esperado: ${parsed.error.issues[0]?.message ?? "sin detalle"}`);
      }

      return {
        data: parsed.data,
        model: payload.model ?? args.model,
        usage: usageFromPayload(payload),
        rawResponse: payload as unknown as Record<string, unknown>,
        warnings,
      };
    } catch (error) {
      lastError = error;

      if (attempt < args.retries) {
        warnings.push({
          code: "retrying_openai_request",
          message: `${getErrorMessage(error)} Reintentando (${attempt + 1}/${args.retries + 1}).`,
        });
        continue;
      }
    } finally {
      clearTimeout(timer);
    }
  }

  throw new Error(getErrorMessage(lastError));
}

export async function callOpenAiJsonObjectFallback<T>(input: OpenAiJsonObjectInput<T>): Promise<AiEnvelope<T>> {
  const model = input.model?.trim() || input.fallbackModel?.trim() || getSmartFallbackModel();
  const timeoutMs = input.timeoutMs ?? parsePositiveInteger(process.env.OPENAI_SMART_TIMEOUT_MS, DEFAULT_TIMEOUT_MS);
  const maxPromptChars = input.maxPromptChars ?? parsePositiveInteger(process.env.OPENAI_SMART_MAX_CHARS, DEFAULT_MAX_PROMPT_CHARS);
  const retries = input.retries ?? DEFAULT_RETRIES;
  const temperature = typeof input.temperature === "number" ? input.temperature : 0;
  const { prompt, truncated } = truncatePrompt(input.userPrompt, maxPromptChars);
  const warnings: AiWarning[] = [...(input.initialWarnings ?? [])];

  if (truncated) {
    warnings.push({
      code: "prompt_truncated",
      message: `El prompt fue truncado a ${maxPromptChars} caracteres para controlar costo y latencia.`,
    });
  }

  const requestJson: Record<string, unknown> = {
    model,
    temperature,
    response_format: {
      type: "json_object",
    },
    messages: [
      { role: "system", content: input.systemPrompt },
      { role: "user", content: prompt },
    ],
  };

  try {
    const result = await executeOpenAiCall({
      promptVersion: input.promptVersion,
      systemPrompt: input.systemPrompt,
      userPrompt: prompt,
      outputSchema: input.outputSchema,
      model,
      responseFormat: {
        type: "json_object",
      },
      timeoutMs,
      retries,
      temperature,
    });

    return {
      data: result.data,
      engine: "openai_json_object",
      model: result.model,
      usage: result.usage,
      warnings: [...warnings, ...result.warnings],
      confidence: inferConfidence(result.data),
      promptVersion: input.promptVersion,
      truncated,
      requestJson,
      responseJson: result.rawResponse,
    };
  } catch (error) {
    warnings.push({
      code: "openai_json_object_failed",
      message: getErrorMessage(error),
    });

    return {
      data: null,
      engine: "empty_assist",
      model: null,
      usage: null,
      warnings,
      confidence: null,
      promptVersion: input.promptVersion,
      truncated,
      requestJson,
      responseJson: null,
    };
  }
}

export async function callOpenAiJsonSchema<T>(input: OpenAiJsonSchemaInput<T>): Promise<AiEnvelope<T>> {
  const model = input.model?.trim() || getSmartModel();
  const timeoutMs = input.timeoutMs ?? parsePositiveInteger(process.env.OPENAI_SMART_TIMEOUT_MS, DEFAULT_TIMEOUT_MS);
  const maxPromptChars = input.maxPromptChars ?? parsePositiveInteger(process.env.OPENAI_SMART_MAX_CHARS, DEFAULT_MAX_PROMPT_CHARS);
  const retries = input.retries ?? DEFAULT_RETRIES;
  const temperature = typeof input.temperature === "number" ? input.temperature : 0;
  const { prompt, truncated } = truncatePrompt(input.userPrompt, maxPromptChars);
  const warnings: AiWarning[] = [];

  if (truncated) {
    warnings.push({
      code: "prompt_truncated",
      message: `El prompt fue truncado a ${maxPromptChars} caracteres para controlar costo y latencia.`,
    });
  }

  const requestJson: Record<string, unknown> = {
    model,
    temperature,
    response_format: {
      type: "json_schema",
      json_schema: {
        name: input.schemaName,
        strict: true,
        schema: input.jsonSchema,
      },
    },
    messages: [
      { role: "system", content: input.systemPrompt },
      { role: "user", content: prompt },
    ],
  };

  try {
    const result = await executeOpenAiCall({
      promptVersion: input.promptVersion,
      systemPrompt: input.systemPrompt,
      userPrompt: prompt,
      outputSchema: input.outputSchema,
      model,
      responseFormat: {
        type: "json_schema",
        json_schema: {
          name: input.schemaName,
          strict: true,
          schema: input.jsonSchema,
        },
      },
      timeoutMs,
      retries,
      temperature,
    });

    return {
      data: result.data,
      engine: "openai_json_schema",
      model: result.model,
      usage: result.usage,
      warnings: [...warnings, ...result.warnings],
      confidence: inferConfidence(result.data),
      promptVersion: input.promptVersion,
      truncated,
      requestJson,
      responseJson: result.rawResponse,
    };
  } catch (error) {
    warnings.push({
      code: "openai_json_schema_failed",
      message: getErrorMessage(error),
    });

    return callOpenAiJsonObjectFallback({
      ...input,
      userPrompt: prompt,
      initialWarnings: warnings,
      model: input.fallbackModel?.trim() || getSmartFallbackModel(),
      timeoutMs,
      maxPromptChars,
      retries,
      temperature,
    });
  }
}
