import type { ProviderCustomModel } from "#cli/compat/providers";
import { saveUserInfo, setOAuthTokens } from "#cli/core/auth";
import {
  buildNextApiOAuthTokenRecord,
  fetchNextApiOAuthModels,
  fetchNextApiOAuthUserInfo,
  NEXT_API_OAUTH_PROVIDER_ID,
  resolveNextApiOAuthBrowserUrl,
  resolveNextApiOAuthConfig,
  startNextApiOAuthServer,
  stopNextApiOAuthServer,
  waitForNextApiOAuthCallback,
} from "#cli/core/next-api-oauth";
import { generatePKCE, generateState } from "#cli/core/oauth";
import {
  loadDimcodeUserCache,
  loadDimcodeUserConfig,
  saveDimcodeUserCache,
  saveDimcodeUserConfig,
} from "#cli/compat/config";
import { openBrowser } from "../cli/open-browser";

type WritableLike = Readonly<{ write: (chunk: string) => unknown }>;
type AuthProvider = "next-api-oauth";

export const NEXT_API_OAUTH_AUTH_METHOD_ID = "next-api-oauth-login";
export const NEXT_API_OAUTH_AUTH_METHOD = {
  description: "Run `dim auth next-api-oauth` in the terminal",
  name: "Login with Next API OAuth",
  id: NEXT_API_OAUTH_AUTH_METHOD_ID,
} as const;

type AuthCommandResolution =
  | Readonly<{ kind: "help" }>
  | Readonly<{ kind: "run"; provider: AuthProvider }>
  | Readonly<{ kind: "error"; message: string }>;

export type NextApiOAuthLoginResult = Readonly<{
  email?: string;
  modelId?: string;
  modelCount: number;
}>;

export type NextApiOAuthLoginOptions = Readonly<{
  env?: Record<string, string | undefined>;
  defaultModelId?: string;
  openUrl?: (url: string) => boolean | Promise<boolean>;
  onInfo?: (message: string) => void;
  onAuthUrl?: (url: string) => void;
  now?: () => number;
}>;

export type RunAuthCommandOptions = Readonly<{
  env?: Record<string, string | undefined>;
  stdout?: WritableLike;
  stderr?: WritableLike;
  openUrl?: (url: string) => boolean | Promise<boolean>;
  loginWithNextApiOAuth?: (options: NextApiOAuthLoginOptions) => Promise<NextApiOAuthLoginResult>;
}>;

function isHelpFlag(arg: string): boolean {
  return arg === "--help" || arg === "-h";
}

function writeLine(stream: WritableLike, message: string): void {
  stream.write(`${message}\n`);
}

function normalizeAuthProvider(raw: string): AuthProvider | undefined {
  const value = String(raw ?? "")
    .trim()
    .toLowerCase()
    .replace(/_/g, "-");
  if (value === "next-api-oauth" || value === "next-api-oath") return "next-api-oauth";
  return undefined;
}

function resolveNextApiOAuthModelId(
  models: readonly ProviderCustomModel[],
  preferred?: string,
): string | undefined {
  const normalized = String(preferred ?? "").trim();
  if (normalized) {
    if (!models.length || models.some((model) => String(model.id ?? "").trim() === normalized)) {
      return normalized;
    }
  }
  const first = String(models[0]?.id ?? "").trim();
  return first || undefined;
}

async function loadPreferredNextApiOAuthModelId(
  env: Record<string, string | undefined>,
): Promise<string | undefined> {
  const [{ config }, { cache }] = await Promise.all([
    loadDimcodeUserConfig(env),
    loadDimcodeUserCache(env),
  ]);
  const settings = (config.settings ?? {}) as Record<string, unknown>;
  const preferred = String(
    (settings.activeModelByProvider as Record<string, unknown> | undefined)?.[
      NEXT_API_OAUTH_PROVIDER_ID
    ] ?? "",
  ).trim();
  if (preferred) return preferred;
  const cacheModel = String(
    (cache.settings as Record<string, unknown> | undefined)?.model ?? "",
  ).trim();
  return cacheModel || undefined;
}

export function getAuthHelpText(): string {
  return [
    "Usage:",
    "  dim auth [provider]",
    "",
    "Providers:",
    "  next-api-oauth    Login with Next API OAuth",
    "",
    "Examples:",
    "  dim auth",
    "  dim auth next-api-oauth",
  ].join("\n");
}

export function resolveAuthCommand(argv: readonly string[]): AuthCommandResolution {
  const positional: string[] = [];

  for (const raw of argv) {
    const arg = String(raw ?? "").trim();
    if (!arg) continue;
    if (isHelpFlag(arg)) return { kind: "help" };
    if (arg.startsWith("-")) return { kind: "error", message: `Unknown option: ${arg}` };
    positional.push(arg);
  }

  if (positional.length === 0) return { kind: "run", provider: "next-api-oauth" };
  if (positional.length > 1) {
    return {
      kind: "error",
      message: `Too many arguments: ${positional.slice(1).join(" ")}`,
    };
  }

  const provider = normalizeAuthProvider(positional[0]);
  if (!provider) {
    return {
      kind: "error",
      message: `Unsupported auth provider: ${positional[0]}. Supported providers: "next-api-oauth".`,
    };
  }

  return { kind: "run", provider };
}

async function persistNextApiOAuthSettings(
  opts: Readonly<{
    env: Record<string, string | undefined>;
    baseUrl: string;
    accessToken: string;
    models: readonly ProviderCustomModel[];
    modelId?: string;
    updatedAt: number;
  }>,
): Promise<void> {
  const { filePath, cache } = await loadDimcodeUserCache(opts.env);
  const nextCacheSettings = { ...cache.settings } as Record<string, unknown>;
  delete nextCacheSettings.authMethod;
  if (opts.modelId) nextCacheSettings.model = opts.modelId;
  const nextCache = {
    ...cache,
    updatedAt: opts.updatedAt,
    settings: nextCacheSettings,
  };
  await saveDimcodeUserCache(filePath, nextCache);

  const { filePath: configPath, config } = await loadDimcodeUserConfig(opts.env);
  const currentSettings = config.settings ?? {};
  const nextConnections = {
    ...currentSettings.providerConnections,
    [NEXT_API_OAUTH_PROVIDER_ID]: {
      baseUrl: opts.baseUrl,
      apiKey: opts.accessToken,
      ...(opts.models.length ? { models: opts.models } : {}),
    },
  };
  const nextActiveModelByProvider = {
    ...currentSettings.activeModelByProvider,
  } as Record<string, string>;
  if (opts.modelId) nextActiveModelByProvider[NEXT_API_OAUTH_PROVIDER_ID] = opts.modelId;

  const nextConfig = {
    ...config,
    updatedAt: opts.updatedAt,
    settings: {
      ...currentSettings,
      providerId: NEXT_API_OAUTH_PROVIDER_ID,
      providerConnections: nextConnections,
      activeModelByProvider: nextActiveModelByProvider,
    },
  };
  await saveDimcodeUserConfig(configPath, nextConfig);
}

export async function runNextApiOAuthLogin(
  options: NextApiOAuthLoginOptions = {},
): Promise<NextApiOAuthLoginResult> {
  const env = options.env ?? (process.env as Record<string, string | undefined>);
  const now = options.now ?? (() => Date.now());

  try {
    options.onInfo?.("Resolving Next API OAuth configuration...");
    const config = await resolveNextApiOAuthConfig(env);

    options.onInfo?.("Starting local OAuth callback server...");
    const { redirectUri } = await startNextApiOAuthServer(config.port);

    options.onInfo?.("Preparing OAuth authorization URL...");
    const pkce = await generatePKCE();
    const state = generateState();
    const authUrl = await resolveNextApiOAuthBrowserUrl(config, redirectUri, pkce, state);

    const opened = options.openUrl ? await Promise.resolve(options.openUrl(authUrl)) : false;
    if (!opened) options.onAuthUrl?.(authUrl);

    options.onInfo?.("Waiting for OAuth callback...");
    const tokenResponse = await waitForNextApiOAuthCallback(pkce, state, config);
    if (!tokenResponse.access_token || !tokenResponse.refresh_token)
      throw new Error("OAuth token response is missing required fields");

    const storedTokens = buildNextApiOAuthTokenRecord(config, tokenResponse);
    await setOAuthTokens("next-api-oauth", storedTokens);

    let models: ProviderCustomModel[] = [];
    try {
      options.onInfo?.("Fetching Next API models...");
      models = await fetchNextApiOAuthModels(storedTokens.access, config.relayBaseUrl);
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      options.onInfo?.(`Skipping model discovery: ${message}`);
    }

    try {
      options.onInfo?.("Fetching user info...");
      const userInfo = await fetchNextApiOAuthUserInfo(storedTokens.access, config.issuer);
      await saveUserInfo(userInfo);
      options.onInfo?.(
        `User: ${userInfo.name || userInfo.preferred_username || userInfo.email || userInfo.sub}`,
      );
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      options.onInfo?.(`Skipping user info fetch: ${message}`);
    }

    const preferredModelId =
      options.defaultModelId ?? (await loadPreferredNextApiOAuthModelId(env));
    const modelId = resolveNextApiOAuthModelId(models, preferredModelId);
    await persistNextApiOAuthSettings({
      env,
      baseUrl: config.relayBaseUrl,
      accessToken: storedTokens.access,
      models,
      modelId,
      updatedAt: now(),
    });

    return {
      email: storedTokens.email,
      modelId,
      modelCount: models.length,
    };
  } finally {
    stopNextApiOAuthServer();
  }
}

export async function runAuthCommand(
  argv: readonly string[],
  options: RunAuthCommandOptions = {},
): Promise<number> {
  const stdout = options.stdout ?? process.stdout;
  const stderr = options.stderr ?? process.stderr;
  const env = options.env ?? (process.env as Record<string, string | undefined>);
  const resolved = resolveAuthCommand(argv);

  if (resolved.kind === "help") {
    writeLine(stdout, getAuthHelpText());
    return 0;
  }

  if (resolved.kind === "error") {
    writeLine(stderr, resolved.message);
    writeLine(stderr, "Run `dim auth --help` for usage.");
    return 1;
  }

  const loginWithNextApiOAuth = options.loginWithNextApiOAuth ?? runNextApiOAuthLogin;
  writeLine(stdout, "Starting Next API OAuth login...");
  try {
    const result = await loginWithNextApiOAuth({
      env,
      openUrl: options.openUrl ?? openBrowser,
      onInfo: (message) => writeLine(stdout, message),
      onAuthUrl: (url) => {
        writeLine(stdout, "Open this URL in your browser to continue login:");
        writeLine(stdout, url);
      },
    });
    if (result.email) writeLine(stdout, `Next API account: ${result.email}`);
    writeLine(stdout, `Discovered models: ${result.modelCount}`);
    if (result.modelId) writeLine(stdout, `Selected model: ${result.modelId}`);
    writeLine(stdout, "Next API OAuth authentication complete.");
    return 0;
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    writeLine(stderr, `Next API OAuth login failed: ${message}`);
    return 1;
  }
}
