import type {
  AiConnectionResult,
  AiContentType,
  AiGenerationMode,
  AiPromptPack
} from "../../shared/src";

export type AiPromptTemplateSeed = {
  packId: string;
  contentType: AiContentType;
  mode: AiGenerationMode;
  name: string;
  template: string;
};

export type AiProviderConfig = {
  baseUrl: string;
  apiKey: string | null;
  model: string;
  contentType?: AiContentType;
};

type ChatCompletionResponse = {
  model?: string;
  choices?: Array<{
    finish_reason?: string | null;
    message?: {
      content?: string | Array<{ type?: string; text?: string }> | null;
    };
  }>;
  error?: {
    message?: string;
  };
};

type ModelListResponse = {
  data?: Array<{ id?: string }>;
  error?: { message?: string };
};

const OUTPUT_CONTRACTS: Record<AiContentType, string> = {
  article:
    '{"title":"factual headline","body":"6-10 paragraph article","dek":"one-sentence summary"}',
  recap:
    '{"title":"factual recap headline","body":"2-4 paragraph recap"}',
  headline:
    '{"headline":"short factual headline","subhead":"one factual supporting sentence"}',
  social:
    '{"posts":[{"account":"personality name","role":"beat_reporter|national_analyst|fan","text":"post"}]}',
  grade_explanation:
    '{"explanations":[{"playerName":"name","score":0,"gradeLabel":"label","explanation":"2-3 factual sentences"}]}'
};

const PACK_VOICES: Record<string, string> = {
  "serious-newsroom":
    "Write with restrained, precise sports-journalism language. Prioritize verified facts and clear attribution.",
  balanced:
    "Write with an accessible modern sports voice. Be energetic when the facts support it, but stay measured and specific.",
  "chaotic-social":
    "Use a lively digital-sports voice with distinct personalities. Keep every factual claim anchored to the evidence."
};

export const AI_PROMPT_PACKS: AiPromptPack[] = [
  {
    id: "serious-newsroom",
    name: "Serious Newsroom",
    description: "Measured reporting, conservative claims, and traditional sports copy.",
    personalities: [
      {
        id: "marcus-hale",
        name: "Marcus Hale",
        role: "beat_reporter",
        voice: "Methodical local beat reporter who emphasizes coaching decisions and verified detail.",
        enabled: true
      },
      {
        id: "dana-brooks",
        name: "Dana Brooks",
        role: "national_analyst",
        voice: "Reserved national analyst focused on rankings, opponent quality, and season implications.",
        enabled: true
      },
      {
        id: "bayou-film-room",
        name: "Bayou Film Room",
        role: "fan",
        voice: "Knowledgeable fan account that reacts strongly without inventing information.",
        enabled: true
      }
    ]
  },
  {
    id: "balanced",
    name: "Balanced",
    description: "A modern mix of reporting, analysis, and fan energy.",
    personalities: [
      {
        id: "taylor-reed",
        name: "Taylor Reed",
        role: "beat_reporter",
        voice: "Direct beat reporter who blends key numbers with concise scene-setting.",
        enabled: true
      },
      {
        id: "cam-walker",
        name: "Cam Walker",
        role: "national_analyst",
        voice: "Confident analyst who connects results to the wider college football landscape.",
        enabled: true
      },
      {
        id: "saturday-signal",
        name: "Saturday Signal",
        role: "fan",
        voice: "Enthusiastic fan account with sharp, good-natured reactions.",
        enabled: true
      }
    ]
  },
  {
    id: "chaotic-social",
    name: "Chaotic Social",
    description: "High-energy social reactions with factual guardrails.",
    personalities: [
      {
        id: "jules-carter",
        name: "Jules Carter",
        role: "beat_reporter",
        voice: "Fast-moving sideline reporter with punchy observations and specific stats.",
        enabled: true
      },
      {
        id: "fourth-down-max",
        name: "Fourth Down Max",
        role: "national_analyst",
        voice: "Provocative national personality who makes bold but evidence-supported arguments.",
        enabled: true
      },
      {
        id: "caps-lock-saturday",
        name: "CAPS LOCK SATURDAY",
        role: "fan",
        voice: "Unrestrained fan energy, short sentences, and no unsupported factual claims.",
        enabled: true
      }
    ]
  }
];

export const AI_PROMPT_TEMPLATE_SEEDS: AiPromptTemplateSeed[] =
  AI_PROMPT_PACKS.flatMap((pack) =>
    (Object.keys(OUTPUT_CONTRACTS) as AiContentType[]).flatMap((contentType) =>
      (["save_only", "screenshot_enriched"] as AiGenerationMode[]).map((mode) => ({
        packId: pack.id,
        contentType,
        mode,
        name: `${contentTypeLabel(contentType)} / ${mode === "save_only" ? "Save Only" : "Screenshot Enriched"}`,
        template: defaultPromptTemplate(pack.id, contentType, mode)
      }))
    )
  );

export function normalizeAiBaseUrl(value: string): string {
  const trimmed = value.trim().replace(/\/+$/, "");
  if (!trimmed) return "";
  const parsed = new URL(trimmed);
  if (!["http:", "https:"].includes(parsed.protocol)) {
    throw new Error("AI base URL must use http or https.");
  }
  return parsed.toString().replace(/\/+$/, "");
}

export function renderAiPrompt(
  template: string,
  values: {
    factsJson: string;
    modeGuidance: string;
    personalitiesJson: string;
    outputContract: string;
  }
): string {
  const rendered = template
    .replaceAll("{{facts_json}}", values.factsJson)
    .replaceAll("{{mode_guidance}}", values.modeGuidance)
    .replaceAll("{{personality_json}}", values.personalitiesJson)
    .replaceAll("{{output_contract}}", values.outputContract);
  const unresolved = rendered.match(/\{\{[a-z0-9_]+\}\}/i)?.[0];
  if (unresolved) throw new Error(`Prompt contains unresolved placeholder ${unresolved}.`);
  return rendered;
}

export function outputContractFor(contentType: AiContentType): string {
  return OUTPUT_CONTRACTS[contentType];
}

export async function testAiProvider(
  config: AiProviderConfig,
  fetchImpl: typeof fetch = fetch
): Promise<AiConnectionResult> {
  const baseUrl = normalizeAiBaseUrl(config.baseUrl);
  if (!baseUrl) throw new Error("Configure an AI base URL first.");
  const response = await fetchWithTimeout(
    `${baseUrl}/models`,
    {
      method: "GET",
      headers: providerHeaders(config.apiKey)
    },
    fetchImpl,
    20_000
  );
  const body = (await readJsonResponse(response)) as ModelListResponse;
  if (!response.ok) throw providerError(response.status, body.error?.message);
  const availableModels = (body.data ?? [])
    .flatMap((model) => (model.id?.trim() ? [model.id.trim()] : []))
    .sort();
  return {
    ok: true,
    baseUrl,
    availableModels,
    message: availableModels.length
      ? `Connected. ${availableModels.length} models available.`
      : "Connected. The provider returned no model list."
  };
}

export async function requestAiCompletion(
  config: AiProviderConfig,
  renderedPrompt: string,
  fetchImpl: typeof fetch = fetch
): Promise<{ model: string; content: Record<string, unknown> }> {
  const baseUrl = normalizeAiBaseUrl(config.baseUrl);
  if (!baseUrl) throw new Error("Configure an AI base URL first.");
  if (!config.model.trim()) throw new Error("Configure an AI model first.");

  const requestBody: Record<string, unknown> = {
    model: config.model.trim(),
    messages: [
      {
        role: "system",
        content:
          "You generate audited college football content. Use only supplied evidence. Return exactly one JSON object and no markdown."
      },
      {
        role: "user",
        content: renderedPrompt
      }
    ],
    temperature: 0.2,
    max_tokens: outputTokenLimit(config.contentType),
    reasoning_effort: "none",
    stream: false,
    ...(config.contentType
      ? { response_format: structuredOutputFor(config.contentType) }
      : {})
  };
  let activeRequestBody = requestBody;
  let response: Response;
  let body: ChatCompletionResponse;
  for (let attempt = 0; ; attempt += 1) {
    response = await sendChatCompletion(
      baseUrl,
      config.apiKey,
      activeRequestBody,
      fetchImpl
    );
    body = (await readJsonResponse(response)) as ChatCompletionResponse;
    if (response.ok || attempt >= 2 || ![400, 422].includes(response.status)) break;
    const message = body.error?.message ?? "";
    if (
      Object.hasOwn(activeRequestBody, "reasoning_effort") &&
      /reasoning|unsupported.?parameter|unknown.?parameter/i.test(message)
    ) {
      const fallbackBody = { ...activeRequestBody };
      delete fallbackBody.reasoning_effort;
      activeRequestBody = fallbackBody;
      continue;
    }
    if (
      Object.hasOwn(activeRequestBody, "response_format") &&
      /response.?format|json.?schema|structured/i.test(message)
    ) {
      const fallbackBody = { ...activeRequestBody };
      delete fallbackBody.response_format;
      activeRequestBody = fallbackBody;
      continue;
    }
    break;
  }
  if (!response.ok) throw providerError(response.status, body.error?.message);
  const choice = body.choices?.[0];
  if (choice?.finish_reason === "length") {
    throw new Error("AI provider reached the output token limit before finishing.");
  }
  const text = messageContentText(choice?.message?.content);
  if (!text) throw new Error("AI provider returned an empty completion.");
  return {
    model: body.model?.trim() || config.model.trim(),
    content: parseAiJsonContent(text)
  };
}

export function parseAiJsonContent(value: string): Record<string, unknown> {
  const trimmed = value
    .replace(/<think>[\s\S]*?<\/think>/gi, "")
    .trim();
  const unfenced = trimmed
    .replace(/^```(?:json)?\s*/i, "")
    .replace(/\s*```$/, "")
    .trim();
  const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]?.trim();
  const embedded = extractJsonObject(trimmed);
  for (const candidate of [unfenced, fenced, embedded]) {
    if (!candidate) continue;
    try {
      const parsed = JSON.parse(candidate) as unknown;
      if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
        continue;
      }
      return parsed as Record<string, unknown>;
    } catch {
      // Try the next candidate shape.
    }
  }
  throw new Error("AI provider did not return valid JSON.");
}

function extractJsonObject(value: string): string | null {
  const start = value.indexOf("{");
  if (start < 0) return null;
  let depth = 0;
  let inString = false;
  let escaped = false;
  for (let index = start; index < value.length; index += 1) {
    const character = value[index];
    if (inString) {
      if (escaped) {
        escaped = false;
      } else if (character === "\\") {
        escaped = true;
      } else if (character === '"') {
        inString = false;
      }
      continue;
    }
    if (character === '"') {
      inString = true;
    } else if (character === "{") {
      depth += 1;
    } else if (character === "}") {
      depth -= 1;
      if (depth === 0) return value.slice(start, index + 1);
    }
  }
  return null;
}

export function validateAiArtifactContent(
  contentType: AiContentType,
  content: Record<string, unknown>
): Record<string, unknown> {
  switch (contentType) {
    case "article":
    case "recap":
      requireText(content, "title");
      requireText(content, "body");
      break;
    case "headline":
      requireText(content, "headline");
      break;
    case "social":
      for (const post of requireObjectArray(content, "posts")) {
        requireText(post, "account");
        requireText(post, "role");
        requireText(post, "text");
      }
      break;
    case "grade_explanation":
      for (const explanation of requireObjectArray(content, "explanations")) {
        requireText(explanation, "playerName");
        requireText(explanation, "gradeLabel");
        requireText(explanation, "explanation");
        if (
          typeof explanation.score !== "number" ||
          !Number.isFinite(explanation.score)
        ) {
          throw new Error('AI response has an invalid grade "score".');
        }
      }
      break;
  }
  return content;
}

function defaultPromptTemplate(
  packId: string,
  contentType: AiContentType,
  mode: AiGenerationMode
): string {
  const modeRule =
    mode === "screenshot_enriched"
      ? "Approved screenshot evidence is available. You may describe only the listed highlights and corrected screenshot stats."
      : "No approved screenshot evidence is available. Do not imply play-by-play knowledge or invent turning points, quotes, injuries, or atmosphere.";
  return [
    `VOICE\n${PACK_VOICES[packId]}`,
    `TASK\nCreate a ${contentTypeLabel(contentType).toLowerCase()} from the evidence.`,
    `GROUNDING\n${modeRule}\n{{mode_guidance}}`,
    "Do not invent quotes, injuries, rankings, records, chronology, or causal claims.",
    "If a desired detail is absent, omit it. Formula grades are fixed and must not be changed.",
    "RECURRING PERSONALITIES\n{{personality_json}}",
    "SOURCE EVIDENCE\n{{facts_json}}",
    "OUTPUT\nReturn only JSON matching this shape:\n{{output_contract}}"
  ].join("\n\n");
}

function contentTypeLabel(value: AiContentType): string {
  switch (value) {
    case "article":
      return "Long-form article";
    case "recap":
      return "Short recap";
    case "headline":
      return "Headline";
    case "social":
      return "Social posts";
    case "grade_explanation":
      return "Player grade explanations";
  }
}

function structuredOutputFor(contentType: AiContentType): Record<string, unknown> {
  const text = { type: "string" };
  let schema: Record<string, unknown>;
  switch (contentType) {
    case "article":
      schema = {
        type: "object",
        additionalProperties: false,
        properties: { title: text, body: text, dek: text },
        required: ["title", "body", "dek"]
      };
      break;
    case "recap":
      schema = {
        type: "object",
        additionalProperties: false,
        properties: { title: text, body: text },
        required: ["title", "body"]
      };
      break;
    case "headline":
      schema = {
        type: "object",
        additionalProperties: false,
        properties: { headline: text, subhead: text },
        required: ["headline", "subhead"]
      };
      break;
    case "social":
      schema = {
        type: "object",
        additionalProperties: false,
        properties: {
          posts: {
            type: "array",
            items: {
              type: "object",
              additionalProperties: false,
              properties: { account: text, role: text, text },
              required: ["account", "role", "text"]
            }
          }
        },
        required: ["posts"]
      };
      break;
    case "grade_explanation":
      schema = {
        type: "object",
        additionalProperties: false,
        properties: {
          explanations: {
            type: "array",
            items: {
              type: "object",
              additionalProperties: false,
              properties: {
                playerName: text,
                score: { type: "number" },
                gradeLabel: text,
                explanation: text
              },
              required: ["playerName", "score", "gradeLabel", "explanation"]
            }
          }
        },
        required: ["explanations"]
      };
      break;
  }
  return {
    type: "json_schema",
    json_schema: {
      name: `dynasty_live_${contentType}`,
      strict: true,
      schema
    }
  };
}

function outputTokenLimit(contentType: AiContentType | undefined): number {
  return contentType ? 4096 : 2048;
}

function providerHeaders(apiKey: string | null): Record<string, string> {
  return apiKey?.trim()
    ? { Authorization: `Bearer ${apiKey.trim()}` }
    : {};
}

async function sendChatCompletion(
  baseUrl: string,
  apiKey: string | null,
  body: Record<string, unknown>,
  fetchImpl: typeof fetch
): Promise<Response> {
  return fetchWithTimeout(
    `${baseUrl}/chat/completions`,
    {
      method: "POST",
      headers: {
        ...providerHeaders(apiKey),
        "Content-Type": "application/json"
      },
      body: JSON.stringify(body)
    },
    fetchImpl,
    300_000
  );
}

async function fetchWithTimeout(
  url: string,
  init: RequestInit,
  fetchImpl: typeof fetch,
  timeoutMs: number
): Promise<Response> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
  try {
    return await fetchImpl(url, {
      ...init,
      signal: controller.signal
    });
  } catch (error) {
    if (error instanceof Error && error.name === "AbortError") {
      throw new Error("AI provider request timed out.");
    }
    throw error;
  } finally {
    clearTimeout(timeout);
  }
}

async function readJsonResponse(response: Response): Promise<unknown> {
  const text = await response.text();
  if (!text.trim()) return {};
  try {
    return JSON.parse(text);
  } catch {
    if (!response.ok) {
      throw new Error(`AI provider returned HTTP ${response.status}.`);
    }
    throw new Error("AI provider returned a non-JSON response.");
  }
}

function providerError(status: number, message: string | undefined): Error {
  return new Error(
    `AI provider returned HTTP ${status}${message?.trim() ? `: ${message.trim()}` : "."}`
  );
}

function messageContentText(
  value: string | Array<{ type?: string; text?: string }> | null | undefined
): string {
  if (typeof value === "string") return value.trim();
  if (!Array.isArray(value)) return "";
  return value
    .flatMap((part) => (part.type === "text" && part.text ? [part.text] : []))
    .join("")
    .trim();
}

function requireText(content: Record<string, unknown>, key: string): void {
  if (typeof content[key] !== "string" || !content[key].trim()) {
    throw new Error(`AI response is missing required text field "${key}".`);
  }
}

function requireObjectArray(
  content: Record<string, unknown>,
  key: string
): Array<Record<string, unknown>> {
  const value = content[key];
  if (
    !Array.isArray(value) ||
    !value.length ||
    value.some((item) => !item || typeof item !== "object" || Array.isArray(item))
  ) {
    throw new Error(`AI response is missing required object array "${key}".`);
  }
  return value as Array<Record<string, unknown>>;
}
