import { createEnvReader } from "#cli/core/env";
import {
  inspectProviderEnv,
  readProviderApiKeyForAdapter,
  readProviderBaseUrlForAdapter,
} from "#cli/core/provider-env";
import { safeJson } from "#cli/core/utils";

function isRecord(value: unknown): value is Record<string, unknown> {
  return value != null && typeof value === "object" && !Array.isArray(value);
}

function getProviderHintTarget(
  env: Record<string, string | undefined> | undefined,
  resolvedAdapter: string,
): {
  custom: boolean;
  name: string;
  preferProviderName: boolean;
} | null {
  const reader = createEnvReader(env);
  const providerId = reader.prefixedStr("PROVIDER_ID");
  if (!providerId) return null;

  const providerName =
    reader.prefixedStr("PROVIDER_NAME") ||
    (providerId === "custom" || providerId.startsWith("cp_") ? "Custom Provider" : providerId);
  const custom = providerId === "custom" || providerId.startsWith("cp_");
  const preferProviderName =
    (resolvedAdapter === "anthropic" && providerId !== "anthropic") ||
    (resolvedAdapter === "gemini" && providerId !== "google") ||
    ((resolvedAdapter === "openai" || resolvedAdapter === "openai-responses") &&
      providerId !== "openai");

  return { custom, name: providerName, preferProviderName };
}

/**
 * Render an agent error as a user-facing string (best-effort).
 *
 * This should be safe to call with unknown thrown values and never throw.
 */
export function formatAgentErrorForUi(err: unknown): string {
  if (!err) return "Unknown error.";
  if (err instanceof Error) return err.stack || err.message || String(err);
  if (typeof err === "string") return err.trim() ? err : "Unknown error.";
  if (isRecord(err)) {
    const msg = err.message;
    if (typeof msg === "string" && msg.trim()) {
      const code = err.code;
      const status = err.status;
      const codeText = typeof code === "string" && code.trim() ? code.trim() : "";
      const statusText =
        typeof status === "number" && Number.isFinite(status) ? String(status) : "";
      const suffix =
        codeText || statusText
          ? ` (${[codeText ? `code=${codeText}` : null, statusText ? `status=${statusText}` : null].filter(Boolean).join(", ")})`
          : "";
      return `${msg.trim()}${suffix}`;
    }
    // Handle error objects that only contain a requestId (e.g. Codex API errors
    // that omit error details).
    const requestId = err.requestId;
    if (typeof requestId === "string" && requestId.trim()) {
      return `API error (requestId: ${requestId.trim()}). No error details provided by the server.`;
    }
  }
  try {
    return safeJson(err);
  } catch {
    return String(err);
  }
}

/**
 * Infer a helpful "next step" hint based on a rendered error message.
 */
export function formatAgentErrorHintForUi(
  errText: string,
  env: Record<string, string | undefined> | undefined,
): string | null {
  const providerEnv = inspectProviderEnv(env);
  const adapter = providerEnv.explicitAdapter;
  const { hasAnthropicKey, hasGeminiKey, keyCount } = providerEnv;
  const resolvedAdapter =
    adapter ||
    (keyCount === 1 ? (hasAnthropicKey ? "anthropic" : hasGeminiKey ? "gemini" : "openai") : "");
  const providerTarget = getProviderHintTarget(env, resolvedAdapter);
  const useAnthropic = resolvedAdapter === "anthropic";
  const useGemini = resolvedAdapter === "gemini";
  const baseUrl = resolvedAdapter
    ? readProviderBaseUrlForAdapter(
        env,
        resolvedAdapter as "openai" | "openai-responses" | "gemini" | "anthropic",
      )
    : "";
  const ambiguousAdapter = !adapter && keyCount > 1;
  const s = errText.toLowerCase();
  const statusCode = (() => {
    const match = s.match(/\b(401|403|404|429)\b/);
    return match ? Number(match[1]) : Number.NaN;
  })();
  const plainHttpStatusFailure = /\brequest failed with status \d+\b/.test(s);
  const maybeTokenLimit =
    s.includes("max_tokens") ||
    s.includes("max tokens") ||
    s.includes("context length") ||
    (s.includes("token") && s.includes("limit"));
  if (maybeTokenLimit) return "Hint: use /compact to summarize, or start a new session with /new.";
  if (plainHttpStatusFailure) {
    if (statusCode === 401)
      return "Hint: the remote URL requires authentication (401). Check the URL or use an authenticated source.";
    if (statusCode === 403)
      return "Hint: the remote URL rejected the request (403). The site may block bots or require access.";
    if (statusCode === 404)
      return "Hint: the remote URL returned 404. Verify the page exists or try a different URL.";
    if (statusCode === 429)
      return "Hint: the remote URL is rate limiting requests (429). Wait and retry.";
    return "Hint: the remote URL returned an HTTP error. Verify the URL and retry.";
  }
  if (statusCode === 401) {
    if (ambiguousAdapter)
      return "Hint: multiple provider API keys detected. Set `DIMCODE_PROVIDER_ADAPTER` (or use `/connect`) to choose a provider, then retry.";
    if (providerTarget?.preferProviderName)
      return `Hint: 401 Unauthorized. Verify the API key configured for ${providerTarget.name} via \`${providerTarget.custom ? "/settings" : "/connect"}\`, then retry.`;
    return useAnthropic
      ? "Hint: 401 Unauthorized. Verify `ANTHROPIC_API_KEY` via `/connect` (or `/settings` for custom), then retry."
      : useGemini
        ? "Hint: 401 Unauthorized. Verify `GOOGLE_API_KEY` via `/connect` (or `/settings` for custom), then retry."
        : "Hint: 401 Unauthorized. Verify `OPENAI_API_KEY` via `/connect` (or `/settings` for custom), then retry.";
  }
  if (statusCode === 403) {
    return "Hint: 403 Forbidden. Check key permissions, account/project scope, and model access, then retry.";
  }
  if (statusCode === 404 || s.includes("model_not_found") || s.includes("not found")) {
    return "Hint: check model ID and Base URL path (OpenAI-compatible endpoints usually end with `/v1`), then retry.";
  }
  if (statusCode === 429 || s.includes("rate limit") || s.includes("rate_limit")) {
    return "Hint: rate limited. Wait and retry, or switch key/provider.";
  }
  const maybeMissingKey =
    s.includes("api key") ||
    s.includes("apikey") ||
    s.includes("openai_api_key") ||
    s.includes("google_api_key") ||
    s.includes("anthropic_api_key") ||
    (resolvedAdapter
      ? !readProviderApiKeyForAdapter(
          env,
          resolvedAdapter as "openai" | "openai-responses" | "gemini" | "anthropic",
        )
      : keyCount === 0);
  if (maybeMissingKey) {
    if (ambiguousAdapter)
      return "Hint: multiple provider API keys detected. Set `DIMCODE_PROVIDER_ADAPTER` (or use `/connect`) to choose a provider, then retry.";
    if (providerTarget?.preferProviderName)
      return `Hint: set the API key for ${providerTarget.name} via \`${providerTarget.custom ? "/settings" : "/connect"}\`, then retry.`;
    return useAnthropic
      ? "Hint: set `ANTHROPIC_API_KEY` via `/connect` (or `/settings` for custom), then retry."
      : useGemini
        ? "Hint: set `GOOGLE_API_KEY` via `/connect` (or `/settings` for custom), then retry."
        : "Hint: set `OPENAI_API_KEY` via `/connect` (or `/settings` for custom), then retry.";
  }
  const maybeNetwork =
    s.includes("etimedout") ||
    s.includes("timeout") ||
    s.includes("econnreset") ||
    s.includes("enotfound") ||
    s.includes("fetch failed") ||
    s.includes("connection error") ||
    s.includes("network error") ||
    s.includes("ssl") ||
    s.includes("tls") ||
    s.includes("handshake") ||
    s.includes("certificate");
  if (maybeNetwork) {
    const baseUrlHint = baseUrl ? ` (${baseUrl})` : "";
    return `Hint: check network / proxy / base URL${baseUrlHint}, then retry.`;
  }
  return null;
}
