import { readFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { Value } from "@sinclair/typebox/value";
import { WebSearchConfigSchema, type ExaProviderConfig, type WebSearchConfig } from "./schema.ts";

export interface ResolvedWebSearchConfig {
  path: string;
  defaultProviderName: string;
  defaultProvider: ExaProviderConfig;
  providers: ExaProviderConfig[];
  providersByName: Map<string, ExaProviderConfig>;
}

export class WebSearchConfigError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "WebSearchConfigError";
  }
}

export function getDefaultWebSearchConfigPath() {
  return join(homedir(), ".pi", "agent", "web-search.json");
}

function exampleConfigSnippet() {
  return JSON.stringify(
    {
      defaultProvider: "exa-main",
      providers: [
        {
          name: "exa-main",
          type: "exa",
          apiKey: "exa_...",
        },
      ],
    },
    null,
    2,
  );
}

export function normalizeWebSearchConfig(config: WebSearchConfig, path: string): ResolvedWebSearchConfig {
  const providersByName = new Map<string, ExaProviderConfig>();

  for (const provider of config.providers) {
    if (!provider.apiKey.trim()) {
      throw new WebSearchConfigError(`Provider \"${provider.name}\" in ${path} is missing a literal apiKey.`);
    }
    if (providersByName.has(provider.name)) {
      throw new WebSearchConfigError(`Duplicate provider name \"${provider.name}\" in ${path}.`);
    }
    providersByName.set(provider.name, provider);
  }

  const defaultProvider = providersByName.get(config.defaultProvider);
  if (!defaultProvider) {
    throw new WebSearchConfigError(
      `defaultProvider \"${config.defaultProvider}\" does not match any configured provider in ${path}.`,
    );
  }

  return {
    path,
    defaultProviderName: config.defaultProvider,
    defaultProvider,
    providers: [...providersByName.values()],
    providersByName,
  };
}

export async function loadWebSearchConfig(path = getDefaultWebSearchConfigPath()) {
  let raw: string;
  try {
    raw = await readFile(path, "utf8");
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") {
      throw new WebSearchConfigError(
        `Missing web-search config at ${path}.\nCreate ${path} with contents like:\n${exampleConfigSnippet()}`,
      );
    }
    throw error;
  }

  let parsed: unknown;
  try {
    parsed = JSON.parse(raw);
  } catch (error) {
    throw new WebSearchConfigError(`Invalid JSON in ${path}: ${(error as Error).message}`);
  }

  if (!Value.Check(WebSearchConfigSchema, parsed)) {
    const [firstError] = [...Value.Errors(WebSearchConfigSchema, parsed)];
    throw new WebSearchConfigError(
      `Invalid web-search config at ${path}: ${firstError?.path ?? "/"} ${firstError?.message ?? "failed validation"}`,
    );
  }

  return normalizeWebSearchConfig(parsed as WebSearchConfig, path);
}
