import fs from "node:fs/promises";
import path from "node:path";
import type { CliBackendPreparedExecution } from "openclaw/plugin-sdk/cli-backend";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import {
  GOOGLE_GEMINI_CLI_PROVIDER_ID,
  resolveGeminiCliProfileHome as resolveGeminiCliProfileHomePath,
} from "./gemini-cli-auth-home.js";

const GEMINI_CLI_PROVIDER_ID = GOOGLE_GEMINI_CLI_PROVIDER_ID;
const GOOGLE_PROVIDER_ID = "google";
const VERCEL_AI_GATEWAY_PROVIDER_ID = "vercel-ai-gateway";
const GEMINI_CLI_CREDENTIALS_FILENAME = "gemini-credentials.json";
const GEMINI_CLI_GCA_AUTH_ENV = [
  "GOOGLE_GENAI_USE_GCA",
  "GOOGLE_CLOUD_ACCESS_TOKEN",
  "GOOGLE_APPLICATION_CREDENTIALS",
  "GEMINI_FORCE_ENCRYPTED_FILE_STORAGE",
  "GEMINI_FORCE_FILE_STORAGE",
];
const GEMINI_CLI_API_KEY_AUTH_ENV = [
  ...GEMINI_CLI_GCA_AUTH_ENV,
  "GOOGLE_GENAI_USE_VERTEXAI",
  "GOOGLE_API_KEY",
  "GOOGLE_CLOUD_PROJECT",
  "GOOGLE_CLOUD_PROJECT_ID",
  "GOOGLE_CLOUD_QUOTA_PROJECT",
  "GOOGLE_CLOUD_LOCATION",
  "GOOGLE_GEMINI_BASE_URL",
  "GEMINI_CLI_CUSTOM_HEADERS",
  "GEMINI_API_KEY_AUTH_MECHANISM",
];
const GEMINI_CLI_PROFILE_AUTH_ENV = [...GEMINI_CLI_API_KEY_AUTH_ENV, "GEMINI_API_KEY"];
const GEMINI_CLI_PROFILE_SETTINGS_ENV = ["GEMINI_CLI_SYSTEM_SETTINGS_PATH"];

type GeminiAuthProfileCredential = {
  type: "api_key" | "oauth" | "token";
  provider: string;
  key?: string;
  token?: string;
  access?: string;
  refresh?: string;
  expires?: number;
  idToken?: string;
  projectId?: string;
};

type GeminiOAuthCredential = GeminiAuthProfileCredential & {
  type: "oauth";
  provider: typeof GEMINI_CLI_PROVIDER_ID;
  access: string;
  refresh: string;
  expires: number;
};

type GeminiApiKeyCredential = GeminiAuthProfileCredential & {
  type: "api_key";
  provider: typeof GEMINI_CLI_PROVIDER_ID | typeof GOOGLE_PROVIDER_ID;
  key: string;
};

type GeminiCliAuthHomeContext = {
  agentDir?: string;
  authProfileId?: string;
  systemSettingsPath?: string;
};

type GeminiCliAuthSelectedType = "oauth-personal" | "gemini-api-key";

function normalizeString(value: string | undefined): string | undefined {
  const trimmed = value?.trim();
  return trimmed ? trimmed : undefined;
}

function throwUnsupportedGeminiCredential(credential: GeminiAuthProfileCredential): never {
  if (credential.provider === VERCEL_AI_GATEWAY_PROVIDER_ID) {
    throw new Error(
      "Gemini CLI execution cannot use a vercel-ai-gateway auth profile. Use the OpenClaw vercel-ai-gateway provider instead.",
    );
  }
  throw new Error("Gemini CLI execution requires a google-gemini-cli auth profile.");
}

function throwUnstageableSelectedGeminiProfile(
  ctx: GeminiCliAuthHomeContext,
  credential: GeminiAuthProfileCredential | undefined,
): never {
  const authProfileId = normalizeString(ctx.authProfileId);
  if (!authProfileId) {
    throw new Error("Gemini CLI execution requires a selected auth profile.");
  }
  if (!credential) {
    throw new Error(
      "Gemini CLI auth profile was selected but no credential material was found. Re-authenticate with `openclaw models auth login --provider google-gemini-cli --force`.",
    );
  }
  if (credential.provider !== GEMINI_CLI_PROVIDER_ID) {
    throwUnsupportedGeminiCredential(credential);
  }
  throw new Error(
    "Gemini CLI execution supports google-gemini-cli OAuth or API-key auth profiles. Re-authenticate with `openclaw models auth login --provider google-gemini-cli --force`.",
  );
}

function requireGeminiOAuthCredential(
  credential: GeminiAuthProfileCredential | undefined,
): GeminiOAuthCredential | null {
  if (!credential) {
    return null;
  }
  if (credential.type !== "oauth") {
    return null;
  }
  if (credential.provider !== GEMINI_CLI_PROVIDER_ID) {
    throwUnsupportedGeminiCredential(credential);
  }

  const access = normalizeString(credential.access);
  const refresh = normalizeString(credential.refresh);
  if (
    !access ||
    !refresh ||
    typeof credential.expires !== "number" ||
    !Number.isFinite(credential.expires)
  ) {
    throw new Error(
      "Gemini CLI OAuth profile is missing usable token material. Re-authenticate with `openclaw models auth login --provider google-gemini-cli --force`.",
    );
  }

  return {
    ...credential,
    type: "oauth",
    provider: GEMINI_CLI_PROVIDER_ID,
    access,
    refresh,
    expires: credential.expires,
    idToken: normalizeString(credential.idToken),
    projectId: normalizeString(credential.projectId),
  };
}

function requireGeminiApiKeyCredential(
  credential: GeminiAuthProfileCredential | undefined,
): GeminiApiKeyCredential | null {
  if (!credential) {
    return null;
  }
  if (credential.type !== "api_key") {
    return null;
  }
  if (
    credential.provider !== GEMINI_CLI_PROVIDER_ID &&
    credential.provider !== GOOGLE_PROVIDER_ID
  ) {
    throwUnsupportedGeminiCredential(credential);
  }

  const key = normalizeString(credential.key);
  if (!key) {
    throw new Error("Gemini CLI API-key profile is missing usable key material.");
  }

  return {
    ...credential,
    type: "api_key",
    provider: credential.provider,
    key,
  };
}

function resolveGeminiCliProfileHome(ctx: GeminiCliAuthHomeContext): {
  home: string;
  geminiDir: string;
} {
  const agentDir = normalizeString(ctx.agentDir);
  if (!agentDir) {
    throw new Error("Gemini CLI auth profile execution requires an agent directory.");
  }
  const authProfileId = normalizeString(ctx.authProfileId);
  if (!authProfileId) {
    throw new Error("Gemini CLI auth profile execution requires a selected auth profile.");
  }

  const home = resolveGeminiCliProfileHomePath(agentDir, authProfileId);
  return { home, geminiDir: path.join(home, ".gemini") };
}

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

function readGeminiAuthProfileCredential(
  credential: unknown,
): GeminiAuthProfileCredential | undefined {
  if (!isRecord(credential)) {
    return undefined;
  }
  return credential as GeminiAuthProfileCredential;
}

async function readGeminiCliJsonObject(
  filePath: string | undefined,
): Promise<Record<string, unknown>> {
  const normalized = normalizeString(filePath);
  if (!normalized) {
    return {};
  }
  try {
    const parsed = JSON.parse(await fs.readFile(normalized, "utf8")) as unknown;
    if (!isRecord(parsed)) {
      throw new Error(`Gemini CLI system settings must be a JSON object: ${normalized}`);
    }
    return { ...parsed };
  } catch (error) {
    if (
      error &&
      typeof error === "object" &&
      "code" in error &&
      (error as { code?: unknown }).code === "ENOENT"
    ) {
      return {};
    }
    throw error;
  }
}

function buildGeminiCliAuthSettings(
  selectedType: GeminiCliAuthSelectedType,
): Record<string, unknown> {
  return { security: { auth: { selectedType } } };
}

async function buildGeminiCliSystemSettings(
  ctx: GeminiCliAuthHomeContext,
  selectedType: GeminiCliAuthSelectedType,
): Promise<Record<string, unknown>> {
  const base = await readGeminiCliJsonObject(ctx.systemSettingsPath);
  const security = isRecord(base.security) ? { ...base.security } : {};
  const auth = isRecord(security.auth) ? { ...security.auth } : {};
  const enforcedType = normalizeString(
    typeof auth.enforcedType === "string" ? auth.enforcedType : undefined,
  );
  if (enforcedType && enforcedType !== selectedType) {
    throw new Error(
      `Gemini CLI system settings enforce ${enforcedType} auth, but the selected OpenClaw profile requires ${selectedType}.`,
    );
  }
  security.auth = { ...auth, selectedType };
  return {
    ...base,
    security,
  };
}

async function writeGeminiCliJson(filePath: string, value: unknown): Promise<void> {
  await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, {
    encoding: "utf8",
    mode: 0o600,
  });
  await fs.chmod(filePath, 0o600);
}

async function prepareGeminiCliProfileHome(
  ctx: GeminiCliAuthHomeContext,
  selectedType: GeminiCliAuthSelectedType,
): Promise<{
  home: string;
  geminiDir: string;
  systemSettingsPath: string;
  cleanup: () => Promise<void>;
}> {
  const { home, geminiDir } = resolveGeminiCliProfileHome(ctx);
  await fs.mkdir(geminiDir, { recursive: true, mode: 0o700 });
  await fs.chmod(home, 0o700);
  await fs.chmod(geminiDir, 0o700);
  const settings = buildGeminiCliAuthSettings(selectedType);
  const systemSettings = await buildGeminiCliSystemSettings(ctx, selectedType);
  const systemSettingsDir = await fs.mkdtemp(
    path.join(resolvePreferredOpenClawTmpDir(), "openclaw-gemini-cli-"),
  );
  await fs.chmod(systemSettingsDir, 0o700);
  const systemSettingsPath = path.join(systemSettingsDir, "settings.json");
  try {
    await Promise.all([
      writeGeminiCliJson(path.join(geminiDir, "settings.json"), settings),
      writeGeminiCliJson(path.join(home, "settings.json"), settings),
      writeGeminiCliJson(systemSettingsPath, systemSettings),
    ]);
  } catch (error) {
    await fs.rm(systemSettingsDir, { recursive: true, force: true });
    throw error;
  }
  return {
    home,
    geminiDir,
    systemSettingsPath,
    cleanup: async () => {
      await fs.rm(systemSettingsDir, { recursive: true, force: true });
    },
  };
}

async function clearGeminiCliCachedCredentials(geminiDir: string): Promise<void> {
  // Gemini prefers its token store over oauth_creds.json. Rebuild that store
  // from the selected OpenClaw profile each run so stale CLI auth cannot win.
  await fs.rm(path.join(geminiDir, GEMINI_CLI_CREDENTIALS_FILENAME), { force: true });
}

function buildGeminiCliProjectEnv(projectId: string | undefined): Record<string, string> {
  const normalized = normalizeString(projectId);
  if (!normalized) {
    return {};
  }
  return {
    GOOGLE_CLOUD_PROJECT: normalized,
    GOOGLE_CLOUD_PROJECT_ID: normalized,
    GOOGLE_CLOUD_QUOTA_PROJECT: normalized,
  };
}

async function prepareGeminiCliOAuthHome(
  ctx: GeminiCliAuthHomeContext,
  credential: GeminiAuthProfileCredential | undefined,
): Promise<CliBackendPreparedExecution | null> {
  const oauth = requireGeminiOAuthCredential(credential);
  if (!oauth) {
    return null;
  }

  const { home, geminiDir, systemSettingsPath, cleanup } = await prepareGeminiCliProfileHome(
    ctx,
    "oauth-personal",
  );
  await clearGeminiCliCachedCredentials(geminiDir);
  const idToken = normalizeString(oauth.idToken);
  const oauthCreds: Record<string, string | number> = {
    access_token: oauth.access,
    refresh_token: oauth.refresh,
    expiry_date: oauth.expires,
    token_type: "Bearer",
  };
  if (idToken) {
    oauthCreds.id_token = idToken;
  }

  await writeGeminiCliJson(path.join(geminiDir, "oauth_creds.json"), oauthCreds);

  return {
    env: {
      GEMINI_CLI_HOME: home,
      GEMINI_CLI_SYSTEM_SETTINGS_PATH: systemSettingsPath,
      GEMINI_FORCE_FILE_STORAGE: "true",
      ...buildGeminiCliProjectEnv(oauth.projectId),
    },
    clearEnv: [...GEMINI_CLI_PROFILE_AUTH_ENV, ...GEMINI_CLI_PROFILE_SETTINGS_ENV],
    cleanup,
  };
}

async function prepareGeminiCliApiKeyHome(
  ctx: GeminiCliAuthHomeContext,
  credential: GeminiAuthProfileCredential | undefined,
): Promise<CliBackendPreparedExecution | null> {
  const apiKey = requireGeminiApiKeyCredential(credential);
  if (!apiKey) {
    return null;
  }

  const { home, geminiDir, systemSettingsPath, cleanup } = await prepareGeminiCliProfileHome(
    ctx,
    "gemini-api-key",
  );
  await Promise.all([
    fs.rm(path.join(geminiDir, "oauth_creds.json"), { force: true }),
    clearGeminiCliCachedCredentials(geminiDir),
  ]);
  return {
    env: {
      GEMINI_CLI_HOME: home,
      GEMINI_CLI_SYSTEM_SETTINGS_PATH: systemSettingsPath,
      GEMINI_FORCE_FILE_STORAGE: "true",
      GEMINI_API_KEY: apiKey.key,
    },
    clearEnv: [...GEMINI_CLI_PROFILE_AUTH_ENV, ...GEMINI_CLI_PROFILE_SETTINGS_ENV],
    cleanup,
  };
}

export async function prepareGeminiCliAuthHome(
  ctx: GeminiCliAuthHomeContext,
  credential: unknown,
): Promise<CliBackendPreparedExecution | null> {
  const authCredential = readGeminiAuthProfileCredential(credential);
  const prepared =
    (await prepareGeminiCliOAuthHome(ctx, authCredential)) ??
    (await prepareGeminiCliApiKeyHome(ctx, authCredential));
  if (prepared) {
    return prepared;
  }
  if (normalizeString(ctx.authProfileId)) {
    throwUnstageableSelectedGeminiProfile(ctx, authCredential);
  }
  return null;
}
