import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { getProviders } from "./providers";
import { normalizeApiKey } from "./providers";
import type { ProviderConfig } from "../lib/providerContract";
import { isPlainRecord } from "../lib/objectUtils";

export type ExternalProvider = {
  name: string;
  apiKey: string;
  format: "anthropic" | "openai";
  anthropicBaseUrl: string;
  openaiBaseUrl: string;
  models: string[];
  sourceTool: "claude-code" | "opencode" | "mimo-code";
  alreadyExists: boolean;
};

function readJsonSafe(filePath: string): Record<string, unknown> | null {
  try {
    if (!existsSync(filePath)) return null;
    const raw = readFileSync(filePath, "utf-8");
    const parsed: unknown = JSON.parse(raw);
    return isPlainRecord(parsed) ? parsed : null;
  } catch {
    return null;
  }
}

function getEnvValue(envObj: unknown, key: string): string | undefined {
  if (!isPlainRecord(envObj)) return undefined;
  const val = envObj[key];
  return typeof val === "string" ? val.trim() : undefined;
}

// Map of well-known API hostnames to their proper brand names.
// The key is the domain (second-level domain extracted from the hostname),
// matched case-insensitively.
const KNOWN_PROVIDER_NAMES: Record<string, string> = {
  deepseek: "DeepSeek",
  minimaxi: "MiniMax",
  openai: "OpenAI",
  anthropic: "Anthropic",
  openrouter: "OpenRouter",
  groq: "Groq",
  together: "Together",
  fireworks: "Fireworks",
  bedrock: "Bedrock",
  vertexai: "Vertex AI",
  azure: "Azure",
  moonshot: "Moonshot",
  zhipu: "ZhipuAI",
  qwen: "Qwen",
  baichuan: "Baichuan",
  iflytek: "iFlytek",
  doubao: "Doubao",
  lingyiwanwu: "LingyiWanwu",
  stepfun: "StepFun",
};

function deriveNameFromUrl(url: string): string {
  try {
    const hostname = new URL(url).hostname;
    const parts = hostname.split(".");
    const domain = parts.length >= 2 ? (parts[parts.length - 2] ?? hostname) : hostname;
    const domainLower = domain.toLowerCase();
    // Check known providers first
    if (KNOWN_PROVIDER_NAMES[domainLower] !== undefined) {
      return KNOWN_PROVIDER_NAMES[domainLower];
    }
    return domain.charAt(0).toUpperCase() + domain.slice(1);
  } catch {
    return "Imported Provider";
  }
}

// ── Claude Code ──────────────────────────────────────────

function detectClaudeCodeProviders(): ExternalProvider[] {
  const home = homedir();
  const results: ExternalProvider[] = [];

  // Read global + local settings, local takes precedence
  const globalConfig = readJsonSafe(join(home, ".claude", "settings.json"));
  const localConfig = readJsonSafe(join(home, ".claude", "settings.local.json"));

  const mergedEnv: Record<string, unknown> = {};
  if (isPlainRecord(globalConfig)) {
    const env = globalConfig["env"];
    if (isPlainRecord(env)) {
      Object.assign(mergedEnv, env);
    }
  }
  if (isPlainRecord(localConfig)) {
    const env = localConfig["env"];
    if (isPlainRecord(env)) {
      Object.assign(mergedEnv, env);
    }
  }

  const baseUrl = getEnvValue(mergedEnv, "ANTHROPIC_BASE_URL");
  const authToken =
    getEnvValue(mergedEnv, "ANTHROPIC_AUTH_TOKEN") ?? getEnvValue(mergedEnv, "ANTHROPIC_API_KEY");

  if (baseUrl === undefined || baseUrl === "" || authToken === undefined || authToken === "") {
    return results;
  }

  // Collect unique models
  const modelSet = new Set<string>();
  const modelKeys = [
    "ANTHROPIC_MODEL",
    "ANTHROPIC_DEFAULT_OPUS_MODEL",
    "ANTHROPIC_DEFAULT_SONNET_MODEL",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL",
  ];
  for (const key of modelKeys) {
    const val = getEnvValue(mergedEnv, key);
    if (val !== undefined && val !== "") modelSet.add(val);
  }
  const models = modelSet.size > 0 ? [...modelSet] : ["default"];

  results.push({
    name: deriveNameFromUrl(baseUrl),
    apiKey: normalizeApiKey(authToken),
    format: "anthropic",
    anthropicBaseUrl: baseUrl,
    openaiBaseUrl: "",
    models,
    sourceTool: "claude-code",
    alreadyExists: false, // filled in later
  });

  return results;
}

// ── OpenCode ─────────────────────────────────────────────

function detectOpenCodeProviders(): ExternalProvider[] {
  const home = homedir();
  const results: ExternalProvider[] = [];

  const config = readJsonSafe(join(home, ".config", "opencode", "opencode.json"));
  if (config === null) return results;

  // Read auth.json (separate credentials file)
  const auth = readJsonSafe(join(home, ".local", "share", "opencode", "auth.json"));

  const providersVal = config["provider"];
  if (!isPlainRecord(providersVal)) {
    return results;
  }
  const providers: Record<string, unknown> = providersVal;

  for (const [providerId, providerObj] of Object.entries(providers)) {
    if (!isPlainRecord(providerObj)) continue;

    const options = providerObj["options"];
    const npm: unknown = providerObj["npm"];
    const npmStr = typeof npm === "string" ? npm : "";
    const format: "anthropic" | "openai" = npmStr === "@ai-sdk/anthropic" ? "anthropic" : "openai";

    let baseURL = "";
    let apiKey = "";
    if (isPlainRecord(options)) {
      baseURL = typeof options["baseURL"] === "string" ? options["baseURL"].trim() : "";
      apiKey = typeof options["apiKey"] === "string" ? options["apiKey"].trim() : "";
    }

    // Resolve apiKey: prefer inline, fallback to auth.json
    if (apiKey === "" && isPlainRecord(auth)) {
      const cred = auth[providerId];
      if (isPlainRecord(cred)) {
        const key: unknown = cred["key"];
        if (typeof key === "string" && key !== "") apiKey = key;
      }
    }

    if (baseURL === "" || apiKey === "") continue;

    // Collect models
    const modelSet = new Set<string>();
    const modelsObj = providerObj["models"];

    if (isPlainRecord(modelsObj)) {
      for (const [modelId, modelDef] of Object.entries(modelsObj)) {
        if (typeof modelDef === "string") {
          modelSet.add(modelDef);
        } else if (isPlainRecord(modelDef)) {
          // modelId is the actual model identifier sent to the remote API
          modelSet.add(modelId);
        } else {
          modelSet.add(modelId);
        }
      }
    }
    const models = modelSet.size > 0 ? [...modelSet] : ["default"];

    results.push({
      name: providerId,
      apiKey: normalizeApiKey(apiKey),
      format,
      anthropicBaseUrl: format === "anthropic" ? baseURL : "",
      openaiBaseUrl: format === "openai" ? baseURL : "",
      models,
      sourceTool: "opencode",
      alreadyExists: false,
    });
  }

  return results;
}

// ── MiMo Code ────────────────────────────────────────────

function stripJsoncComments(raw: string): string {
  let result = "";
  let i = 0;
  let inString = false;
  let stringChar = "";

  while (i < raw.length) {
    const ch = raw[i];
    const next = raw[i + 1];

    if (inString) {
      if (ch === "\\" && next !== undefined) {
        result += ch + next;
        i += 2;
        continue;
      }
      if (ch === stringChar) {
        inString = false;
        stringChar = "";
      }
      result += ch;
      i += 1;
      continue;
    }

    if (ch === '"' || ch === "'") {
      inString = true;
      stringChar = ch;
      result += ch;
      i += 1;
      continue;
    }

    if (ch === "/" && next === "/") {
      while (i < raw.length && raw[i] !== "\n") {
        i += 1;
      }
      continue;
    }

    if (ch === "/" && next === "*") {
      i += 2;
      while (i < raw.length - 1 && !(raw[i] === "*" && raw[i + 1] === "/")) {
        i += 1;
      }
      i += 2;
      continue;
    }

    result += ch;
    i += 1;
  }

  return result;
}

function readJsonCSafe(filePath: string): Record<string, unknown> | null {
  try {
    if (!existsSync(filePath)) return null;
    const raw = readFileSync(filePath, "utf-8");
    const stripped = stripJsoncComments(raw);
    const parsed: unknown = JSON.parse(stripped);
    return isPlainRecord(parsed) ? parsed : null;
  } catch {
    return null;
  }
}

function detectMiMoCodeProviders(): ExternalProvider[] {
  const home = homedir();
  const results: ExternalProvider[] = [];

  const config =
    readJsonCSafe(join(home, ".config", "mimocode", "mimocode.jsonc")) ??
    readJsonCSafe(join(home, ".config", "mimocode", "mimocode.json")) ??
    readJsonCSafe(join(home, ".config", "mimocode", "config.jsonc")) ??
    readJsonCSafe(join(home, ".config", "mimocode", "config.json")) ??
    readJsonCSafe(join(home, ".mimocode", "config.jsonc")) ??
    readJsonCSafe(join(home, ".mimocode", "config.json"));

  const auth = readJsonSafe(join(home, ".local", "share", "mimocode", "auth.json"));

  // Process providers from config (if config exists and has a provider section)
  const providersVal = config?.["provider"];
  const configProviders: Record<string, unknown> = isPlainRecord(providersVal) ? providersVal : {};

  for (const [providerId, providerObj] of Object.entries(configProviders)) {
    if (!isPlainRecord(providerObj)) continue;

    const options = providerObj["options"];
    const npm: unknown = providerObj["npm"];
    const npmStr = typeof npm === "string" ? npm : "";
    const format: "anthropic" | "openai" = npmStr === "@ai-sdk/anthropic" ? "anthropic" : "openai";

    let baseURL = "";
    let apiKey = "";
    if (isPlainRecord(options)) {
      baseURL = typeof options["baseURL"] === "string" ? options["baseURL"].trim() : "";
      apiKey = typeof options["apiKey"] === "string" ? options["apiKey"].trim() : "";
    }

    if (apiKey === "" && isPlainRecord(auth)) {
      const cred = auth[providerId];
      if (isPlainRecord(cred)) {
        const key: unknown = cred["key"];
        if (typeof key === "string" && key !== "") apiKey = key;
      }
    }

    if (baseURL === "" || apiKey === "") continue;

    const modelSet = new Set<string>();
    const modelsObj = providerObj["models"];

    if (isPlainRecord(modelsObj)) {
      for (const [modelId, modelDef] of Object.entries(modelsObj)) {
        if (typeof modelDef === "string") {
          modelSet.add(modelDef);
        } else if (isPlainRecord(modelDef)) {
          modelSet.add(modelId);
        } else {
          modelSet.add(modelId);
        }
      }
    }
    const models = modelSet.size > 0 ? [...modelSet] : ["default"];

    results.push({
      name: providerId,
      apiKey: normalizeApiKey(apiKey),
      format,
      anthropicBaseUrl: format === "anthropic" ? baseURL : "",
      openaiBaseUrl: format === "openai" ? baseURL : "",
      models,
      sourceTool: "mimo-code",
      alreadyExists: false,
    });
  }

  // Scan orphan auth.json entries not referenced in config
  if (isPlainRecord(auth)) {
    const knownDefaults: Record<
      string,
      { anthropicBaseUrl: string; openaiBaseUrl: string; format: "anthropic" | "openai" }
    > = {
      deepseek: {
        anthropicBaseUrl: "https://api.deepseek.com/anthropic",
        openaiBaseUrl: "https://api.deepseek.com",
        format: "anthropic",
      },
      minimax: {
        anthropicBaseUrl: "https://api.minimaxi.com/anthropic",
        openaiBaseUrl: "https://api.minimaxi.com",
        format: "anthropic",
      },
      "minimax-coding-plan": {
        anthropicBaseUrl: "https://api.minimaxi.com/anthropic",
        openaiBaseUrl: "https://api.minimaxi.com",
        format: "anthropic",
      },
      openai: {
        anthropicBaseUrl: "",
        openaiBaseUrl: "https://api.openai.com",
        format: "openai",
      },
      anthropic: {
        anthropicBaseUrl: "https://api.anthropic.com",
        openaiBaseUrl: "",
        format: "anthropic",
      },
      groq: {
        anthropicBaseUrl: "",
        openaiBaseUrl: "https://api.groq.com/openai",
        format: "openai",
      },
      openrouter: {
        anthropicBaseUrl: "",
        openaiBaseUrl: "https://openrouter.ai/api",
        format: "openai",
      },
      together: {
        anthropicBaseUrl: "",
        openaiBaseUrl: "https://api.together.xyz",
        format: "openai",
      },
    };

    for (const [key, cred] of Object.entries(auth)) {
      if (configProviders[key] !== undefined) continue;
      if (!isPlainRecord(cred)) continue;
      const apiKey: unknown = cred["key"];
      if (typeof apiKey !== "string" || apiKey === "") continue;

      let defaults = knownDefaults[key.toLowerCase()];
      if (defaults === undefined) {
        for (const [knownKey, cfg] of Object.entries(knownDefaults)) {
          if (key.toLowerCase().startsWith(knownKey)) {
            defaults = cfg;
            break;
          }
        }
      }
      if (defaults === undefined) continue;

      results.push({
        name: key,
        apiKey: normalizeApiKey(apiKey),
        format: defaults.format,
        anthropicBaseUrl: defaults.anthropicBaseUrl,
        openaiBaseUrl: defaults.openaiBaseUrl,
        models: ["default"],
        sourceTool: "mimo-code",
        alreadyExists: false,
      });
    }
  }

  return results;
}

// ── Combined scanner ─────────────────────────────────────

export function scanExternalProviders(): { providers: ExternalProvider[]; warnings: string[] } {
  const warnings: string[] = [];

  let claudeProviders: ExternalProvider[] = [];
  try {
    claudeProviders = detectClaudeCodeProviders();
  } catch (err) {
    warnings.push(`Claude Code: ${err instanceof Error ? err.message : String(err)}`);
  }

  let opencodeProviders: ExternalProvider[] = [];
  try {
    opencodeProviders = detectOpenCodeProviders();
  } catch (err) {
    warnings.push(`OpenCode: ${err instanceof Error ? err.message : String(err)}`);
  }

  let mimoProviders: ExternalProvider[] = [];
  try {
    mimoProviders = detectMiMoCodeProviders();
  } catch (err) {
    warnings.push(`MiMo Code: ${err instanceof Error ? err.message : String(err)}`);
  }

  const allProviders = [...claudeProviders, ...opencodeProviders, ...mimoProviders];

  // Filter out providers already using the localhost proxy (already switched)
  const filteredProviders = allProviders.filter((p) => {
    const url = p.anthropicBaseUrl || p.openaiBaseUrl;
    return !/(?:localhost|127\.0\.0\.1)/.test(url);
  });

  // Mark already-existing providers (same apiKey + format-specific baseUrl)
  let existing: ProviderConfig[] = [];
  try {
    existing = getProviders();
  } catch {
    // If we can't load existing providers, just skip the duplicate check
  }

  for (const ext of filteredProviders) {
    ext.alreadyExists = existing.some((p) => {
      if (p.apiKey !== ext.apiKey) return false;
      // Match on the format-specific base URL (external providers only set one)
      if (ext.format === "anthropic") {
        return (p.anthropicBaseUrl ?? "") === ext.anthropicBaseUrl;
      }
      return (p.openaiBaseUrl ?? "") === ext.openaiBaseUrl;
    });
  }

  return { providers: filteredProviders, warnings };
}
