import OpenAI from "openai";
import type { z } from "zod";

export type LlmTaskName =
  | "unknown"
  | "fact_extractor"
  | "article_optimizer"
  | "quality_inspector"
  | "targeted_rewriter";

export interface GenerateInput {
  system?: string;
  prompt: string;
  model?: string;
  temperature?: number;
  task?: LlmTaskName;
}

export interface GenerateValidatedJsonInput<T> extends GenerateInput {
  schema: z.ZodType<T>;
}

export interface LlmProviderStatus {
  provider: "deepseek" | "openai";
  configured: boolean;
  model: string;
  baseURL?: string;
  reason?: string;
}

export class LlmValidationError extends Error {
  constructor(
    message: string,
    public readonly task: LlmTaskName,
  ) {
    super(message);
    this.name = "LlmValidationError";
  }
}

function getProvider() {
  return (process.env.LLM_PROVIDER || "deepseek").toLowerCase();
}

export function getLlmProviderStatus(): LlmProviderStatus {
  const provider = getProvider();

  if (provider === "openai") {
    return {
      provider: "openai",
      configured: Boolean(process.env.OPENAI_API_KEY),
      model: process.env.OPENAI_MODEL || "gpt-4.1-mini",
      reason: process.env.OPENAI_API_KEY ? undefined : "OPENAI_API_KEY is missing",
    };
  }

  return {
    provider: "deepseek",
    configured: Boolean(process.env.DEEPSEEK_API_KEY),
    model: process.env.DEEPSEEK_MODEL || "deepseek-v4-pro",
    baseURL: process.env.DEEPSEEK_BASE_URL || "https://api.deepseek.com",
    reason: process.env.DEEPSEEK_API_KEY
      ? undefined
      : "DEEPSEEK_API_KEY is missing",
  };
}

export function isLlmConfigured() {
  return getLlmProviderStatus().configured;
}

interface ChatCompletionResult {
  choices: Array<{ message: { content?: string | null } }>;
}

type ChatCompletionRequest = {
  model: string;
  temperature: number;
  response_format?: { type: "json_object" };
  messages: Array<{ role: "system" | "user"; content: string }>;
};

let chatCompletionForTesting:
  | ((request: ChatCompletionRequest) => Promise<ChatCompletionResult>)
  | null = null;

export function setChatCompletionForTesting(
  handler: ((request: ChatCompletionRequest) => Promise<ChatCompletionResult>) | null,
) {
  chatCompletionForTesting = handler;
}

function getTask(input: GenerateInput): LlmTaskName {
  return input.task || "unknown";
}

function getRawLogLimit() {
  const parsed = Number(process.env.LLM_LOG_RAW_LIMIT ?? "4000");
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : 4000;
}

function stringifyForLog(value: unknown) {
  return typeof value === "string" ? value : JSON.stringify(value);
}

function truncateRaw(value: string, maxLength = getRawLogLimit()) {
  return value.length > maxLength
    ? `${value.slice(0, maxLength)}...[truncated ${value.length - maxLength} chars]`
    : value;
}

function summarizeZodError(error: z.ZodError) {
  return error.issues
    .slice(0, 5)
    .map((issue) => {
      const path = issue.path.length > 0 ? issue.path.join(".") : "<root>";
      return `${path}: ${issue.message}`;
    })
    .join("; ");
}

function quoteLogValue(value: string) {
  return JSON.stringify(value);
}

function createClient() {
  const status = getLlmProviderStatus();

  if (!status.configured) {
    throw new Error(status.reason ?? "LLM provider is not configured");
  }

  if (status.provider === "openai") {
    return {
      client: new OpenAI({ apiKey: process.env.OPENAI_API_KEY }),
      model: status.model,
    };
  }

  return {
    client: new OpenAI({
      apiKey: process.env.DEEPSEEK_API_KEY,
      baseURL: status.baseURL,
    }),
    model: status.model,
  };
}

export async function generateText(input: GenerateInput) {
  try {
    const { client, model } = createClient();
    const response = await client.chat.completions.create({
      model: input.model ?? model,
      temperature: input.temperature ?? 0.2,
      messages: [
        ...(input.system ? [{ role: "system" as const, content: input.system }] : []),
        { role: "user" as const, content: input.prompt },
      ],
    });

    return response.choices[0]?.message.content ?? "";
  } catch (error) {
    throw normalizeLlmError(error);
  }
}

export async function generateJson<T>(input: GenerateInput): Promise<T> {
  const task = getTask(input);
  const startedAt = Date.now();
  try {
    const status = getLlmProviderStatus();
    if (!status.configured) {
      throw new Error(status.reason ?? "LLM provider is not configured");
    }
    const effectiveModel = input.model ?? status.model;
    console.info(
      `[llm:start] provider=${status.provider} model=${effectiveModel} task=${task}`,
    );
    const request = {
      model: effectiveModel,
      temperature: input.temperature ?? 0.1,
      response_format: { type: "json_object" },
      messages: [
        ...(input.system ? [{ role: "system" as const, content: input.system }] : []),
        { role: "user" as const, content: input.prompt },
      ],
    } satisfies ChatCompletionRequest;
    const response = chatCompletionForTesting
      ? await chatCompletionForTesting(request)
      : await createClient().client.chat.completions.create(request);
    const content = response.choices[0]?.message.content ?? "{}";
    console.info(
      `[llm:response] task=${task} duration_ms=${Date.now() - startedAt} raw=${truncateRaw(content)}`,
    );
    return JSON.parse(content) as T;
  } catch (error) {
    const normalized = normalizeLlmError(error);
    console.error(
      `[llm:error] task=${task} duration_ms=${Date.now() - startedAt} message=${quoteLogValue(normalized.message)}`,
    );
    throw normalized;
  }
}

export let generateJsonForValidation: <T>(input: GenerateInput) => Promise<T> =
  generateJson;

export function setGenerateJsonForValidation(
  generator: typeof generateJsonForValidation,
) {
  generateJsonForValidation = generator;
}

export async function generateValidatedJson<T>({
  schema,
  ...input
}: GenerateValidatedJsonInput<T>): Promise<T> {
  const task = getTask(input);
  if (!isLlmConfigured()) {
    console.info(`[llm:validated] task=${task} ok=false reason=not_configured`);
    throw new LlmValidationError(
      getLlmProviderStatus().reason ?? "LLM provider is not configured",
      task,
    );
  }

  const usesDefaultGenerator = generateJsonForValidation === generateJson;
  const status = getLlmProviderStatus();
  const startedAt = Date.now();
  if (!usesDefaultGenerator) {
    console.info(
      `[llm:start] provider=${status.provider} model=${input.model ?? status.model} task=${task}`,
    );
  }

  try {
    const generated = await generateJsonForValidation<unknown>(input);
    if (!usesDefaultGenerator) {
      console.info(
        `[llm:response] task=${task} duration_ms=${Date.now() - startedAt} raw=${truncateRaw(stringifyForLog(generated))}`,
      );
    }
    const parsed = schema.safeParse(generated);
    if (parsed.success) {
      console.info(`[llm:validated] task=${task} ok=true`);
      return parsed.data;
    }
    console.warn(
      `[llm:validated] task=${task} ok=false zod_error=${quoteLogValue(summarizeZodError(parsed.error))}`,
    );
    throw new LlmValidationError(
      `LLM response failed schema validation: ${summarizeZodError(parsed.error)}`,
      task,
    );
  } catch (error) {
    if (error instanceof LlmValidationError) {
      throw error;
    }
    console.info(`[llm:validated] task=${task} ok=false reason=provider_error`);
    const message = error instanceof Error ? error.message : String(error);
    console.warn(
      usesDefaultGenerator
        ? `[llm:error] task=${task} message=${quoteLogValue(message)}`
        : `[llm:error] task=${task} duration_ms=${Date.now() - startedAt} message=${quoteLogValue(message)}`,
    );
    throw error instanceof Error ? error : new Error(message);
  }
}

function normalizeLlmError(error: unknown) {
  const message = error instanceof Error ? error.message : "Unknown LLM error";
  return new Error(`LLM provider error: ${message}`);
}
