import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import {
  parseOpenAiChatGptTokenInfo,
  refreshOpenAiChatGptTokenData,
} from '../ts/smartai.auth.openai.js';
import type {
  IOpenAiChatGptAuthOptions,
  IOpenAiChatGptTokenData,
  IOpenAiChatGptTokenInfo,
} from '../ts/smartai.interfaces.js';

export type TOpenAiChatGptAuthSource = 'smartai' | 'opencode' | 'codex';
export type TOpenAiChatGptAuthFileFormat = TOpenAiChatGptAuthSource | 'auto';

export interface IOpenAiChatGptAuthSourceConfig {
  source: TOpenAiChatGptAuthSource;
  filePath?: string;
  format?: TOpenAiChatGptAuthFileFormat;
  writeBack?: boolean;
}

export interface IOpenAiChatGptAuthSourceInspection {
  source: TOpenAiChatGptAuthSource;
  filePath: string;
  exists: boolean;
  usable: boolean;
  expired?: boolean;
  accountId?: string;
  email?: string;
  plan?: string;
  expiresAt?: string;
  error?: string;
}

export interface IInspectOpenAiChatGptAuthSourcesOptions {
  sources?: Array<TOpenAiChatGptAuthSource | IOpenAiChatGptAuthSourceConfig>;
  homeDir?: string;
  now?: Date;
}

export interface IResolveOpenAiChatGptAuthOptions extends IInspectOpenAiChatGptAuthSourcesOptions {
  refresh?: 'ifNeeded' | false;
  writeBack?: Partial<Record<TOpenAiChatGptAuthSource, boolean>>;
  authOptions?: IOpenAiChatGptAuthOptions;
}

export interface IResolvedOpenAiChatGptAuth {
  source: TOpenAiChatGptAuthSource;
  filePath: string;
  tokenData: IOpenAiChatGptTokenData;
  refreshed: boolean;
}

interface INormalizedOpenAiChatGptAuthSourceConfig {
  source: TOpenAiChatGptAuthSource;
  filePath: string;
  format: TOpenAiChatGptAuthFileFormat;
  writeBack?: boolean;
}

const defaultSources: TOpenAiChatGptAuthSource[] = ['smartai', 'opencode', 'codex'];
const refreshWindowMs = 5 * 60 * 1000;

export const getDefaultOpenAiChatGptAuthPath = (
  source: TOpenAiChatGptAuthSource,
  homeDir = os.homedir(),
): string => {
  switch (source) {
    case 'smartai':
      return path.join(homeDir, '.git.zone', 'ide', 'openai-chatgpt-auth.json');
    case 'opencode':
      return path.join(homeDir, '.local', 'share', 'opencode', 'auth.json');
    case 'codex':
      return path.join(homeDir, '.codex', 'auth.json');
    default:
      throw new Error(`Unsupported OpenAI ChatGPT auth source: ${source satisfies never}`);
  }
};

export const normalizeOpenAiChatGptAuth = (
  input: unknown,
  format: TOpenAiChatGptAuthFileFormat = 'auto',
): IOpenAiChatGptTokenData | undefined => {
  if (format === 'auto') {
    return normalizeOpenAiChatGptAuth(input, 'smartai')
      ?? normalizeOpenAiChatGptAuth(input, 'opencode')
      ?? normalizeOpenAiChatGptAuth(input, 'codex');
  }

  if (!isRecord(input)) return undefined;
  if (format === 'opencode') {
    return normalizeOpenCodeAuth(input);
  }
  return normalizeTokenObject(isRecord(input.tokens) ? input.tokens : input);
};

export const readOpenAiChatGptAuthFile = async (
  filePath: string,
  format: TOpenAiChatGptAuthFileFormat = 'auto',
): Promise<IOpenAiChatGptTokenData | undefined> => {
  const parsed = JSON.parse(await fs.readFile(filePath, 'utf8')) as unknown;
  return normalizeOpenAiChatGptAuth(parsed, format);
};

export const inspectOpenAiChatGptAuthSources = async (
  options: IInspectOpenAiChatGptAuthSourcesOptions = {},
): Promise<IOpenAiChatGptAuthSourceInspection[]> => {
  const now = options.now ?? new Date();
  const sourceConfigs = normalizeSourceConfigs(options.sources, options.homeDir);
  return Promise.all(sourceConfigs.map(async (sourceConfig) => {
    try {
      const tokenData = await readOpenAiChatGptAuthFile(sourceConfig.filePath!, sourceConfig.format ?? sourceConfig.source);
      if (!tokenData) {
        return {
          source: sourceConfig.source,
          filePath: sourceConfig.filePath!,
          exists: true,
          usable: false,
          error: 'No OpenAI ChatGPT auth token found in file.',
        };
      }
      return toInspection(sourceConfig.source, sourceConfig.filePath!, tokenData, now);
    } catch (error) {
      const nodeError = error as NodeJS.ErrnoException;
      return {
        source: sourceConfig.source,
        filePath: sourceConfig.filePath!,
        exists: nodeError.code !== 'ENOENT',
        usable: false,
        error: nodeError.code === 'ENOENT' ? undefined : nodeError.message,
      };
    }
  }));
};

export const resolveOpenAiChatGptAuth = async (
  options: IResolveOpenAiChatGptAuthOptions = {},
): Promise<IResolvedOpenAiChatGptAuth | undefined> => {
  const now = options.now ?? new Date();
  const sourceConfigs = normalizeSourceConfigs(options.sources, options.homeDir);
  const refresh = options.refresh ?? 'ifNeeded';

  for (const sourceConfig of sourceConfigs) {
    let tokenData: IOpenAiChatGptTokenData | undefined;
    try {
      tokenData = await readOpenAiChatGptAuthFile(sourceConfig.filePath!, sourceConfig.format ?? sourceConfig.source);
    } catch (error) {
      const nodeError = error as NodeJS.ErrnoException;
      if (nodeError.code === 'ENOENT') continue;
      continue;
    }
    if (!tokenData) continue;

    const shouldRefresh = refresh === 'ifNeeded' && shouldRefreshToken(tokenData, now);
    const writeBack = sourceConfig.writeBack ?? options.writeBack?.[sourceConfig.source] ?? sourceConfig.source === 'smartai';
    if (!shouldRefresh) {
      return { source: sourceConfig.source, filePath: sourceConfig.filePath!, tokenData, refreshed: false };
    }

    if (!writeBack) {
      if (!isExpired(tokenData, now)) {
        return { source: sourceConfig.source, filePath: sourceConfig.filePath!, tokenData, refreshed: false };
      }
      continue;
    }

    try {
      const refreshed = await refreshOpenAiChatGptTokenData(tokenData, options.authOptions ?? {});
      await writeOpenAiChatGptAuthFile(sourceConfig.filePath!, refreshed, sourceConfig.format ?? sourceConfig.source);
      return { source: sourceConfig.source, filePath: sourceConfig.filePath!, tokenData: refreshed, refreshed: true };
    } catch {
      continue;
    }
  }

  return undefined;
};

export const writeOpenAiChatGptAuthFile = async (
  filePath: string,
  tokenData: IOpenAiChatGptTokenData,
  format: TOpenAiChatGptAuthFileFormat = 'smartai',
): Promise<void> => {
  const current = await readJsonFileIfExists(filePath);
  const payload = format === 'opencode'
    ? toOpenCodeAuthFile(current, tokenData)
    : format === 'codex'
      ? toCodexAuthFile(current, tokenData)
      : toSmartAiAuthFile(tokenData);
  await writeJsonAtomic(filePath, payload);
};

const normalizeSourceConfigs = (
  sources: Array<TOpenAiChatGptAuthSource | IOpenAiChatGptAuthSourceConfig> | undefined,
  homeDir = os.homedir(),
): INormalizedOpenAiChatGptAuthSourceConfig[] => {
  return (sources ?? defaultSources).map((sourceInput) => {
    const sourceConfig = typeof sourceInput === 'string' ? { source: sourceInput } : sourceInput;
    return {
      source: sourceConfig.source,
      filePath: sourceConfig.filePath ?? getDefaultOpenAiChatGptAuthPath(sourceConfig.source, homeDir),
      format: sourceConfig.format ?? sourceConfig.source,
      writeBack: sourceConfig.writeBack,
    };
  });
};

const toInspection = (
  source: TOpenAiChatGptAuthSource,
  filePath: string,
  tokenData: IOpenAiChatGptTokenData,
  now: Date,
): IOpenAiChatGptAuthSourceInspection => {
  const expired = isExpired(tokenData, now);
  return {
    source,
    filePath,
    exists: true,
    usable: !expired,
    expired,
    accountId: tokenData.accountId ?? tokenData.tokenInfo.chatgptAccountId,
    email: tokenData.tokenInfo.email,
    plan: tokenData.tokenInfo.chatgptPlanType,
    expiresAt: tokenData.tokenInfo.expiresAt,
  };
};

const normalizeTokenObject = (input: Record<string, unknown>): IOpenAiChatGptTokenData | undefined => {
  const accessToken = stringValue(input.accessToken) ?? stringValue(input.access_token);
  const refreshToken = stringValue(input.refreshToken) ?? stringValue(input.refresh_token);
  const idToken = stringValue(input.idToken) ?? stringValue(input.id_token) ?? stringValue((input.tokenInfo as Record<string, unknown> | undefined)?.rawJwt);
  const accountId = stringValue(input.accountId) ?? stringValue(input.account_id);
  return createTokenDataFromValues({ accessToken, refreshToken, idToken, accountId });
};

const normalizeOpenCodeAuth = (input: Record<string, unknown>): IOpenAiChatGptTokenData | undefined => {
  if (!isRecord(input.openai)) return undefined;
  const accessToken = stringValue(input.openai.access);
  const refreshToken = stringValue(input.openai.refresh);
  const accountId = stringValue(input.openai.accountId);
  const expiresAt = typeof input.openai.expires === 'number' ? new Date(input.openai.expires).toISOString() : undefined;
  return createTokenDataFromValues({ accessToken, refreshToken, accountId, fallbackExpiresAt: expiresAt });
};

const createTokenDataFromValues = (input: {
  accessToken?: string;
  refreshToken?: string;
  idToken?: string;
  accountId?: string;
  fallbackExpiresAt?: string;
}): IOpenAiChatGptTokenData | undefined => {
  if (!input.accessToken || !input.refreshToken) return undefined;
  const parseToken = input.idToken ?? input.accessToken;
  const tokenInfo = parseTokenInfo(parseToken, input.accountId, input.fallbackExpiresAt);
  return {
    accessToken: input.accessToken,
    refreshToken: input.refreshToken,
    idToken: input.idToken,
    accountId: input.accountId ?? tokenInfo.chatgptAccountId,
    tokenInfo,
  };
};

const parseTokenInfo = (token: string, accountId?: string, fallbackExpiresAt?: string): IOpenAiChatGptTokenInfo => {
  try {
    const tokenInfo = parseOpenAiChatGptTokenInfo(token);
    return {
      ...tokenInfo,
      chatgptAccountId: tokenInfo.chatgptAccountId ?? accountId,
      expiresAt: tokenInfo.expiresAt ?? fallbackExpiresAt,
    };
  } catch {
    return {
      chatgptAccountId: accountId,
      chatgptAccountIsFedramp: false,
      expiresAt: fallbackExpiresAt,
      rawJwt: token,
    };
  }
};

const toSmartAiAuthFile = (tokenData: IOpenAiChatGptTokenData): Record<string, unknown> => ({
  accessToken: tokenData.accessToken,
  refreshToken: tokenData.refreshToken,
  idToken: tokenData.idToken,
  accountId: tokenData.accountId,
  tokenInfo: tokenData.tokenInfo,
});

const toCodexAuthFile = (current: Record<string, unknown>, tokenData: IOpenAiChatGptTokenData): Record<string, unknown> => ({
  ...current,
  OPENAI_API_KEY: current.OPENAI_API_KEY ?? null,
  tokens: {
    ...(isRecord(current.tokens) ? current.tokens : {}),
    id_token: tokenData.idToken,
    access_token: tokenData.accessToken,
    refresh_token: tokenData.refreshToken,
    account_id: tokenData.accountId ?? tokenData.tokenInfo.chatgptAccountId,
  },
  last_refresh: new Date().toISOString(),
});

const toOpenCodeAuthFile = (current: Record<string, unknown>, tokenData: IOpenAiChatGptTokenData): Record<string, unknown> => ({
  ...current,
  openai: {
    ...(isRecord(current.openai) ? current.openai : {}),
    type: 'oauth',
    refresh: tokenData.refreshToken,
    access: tokenData.accessToken,
    expires: tokenData.tokenInfo.expiresAt ? Date.parse(tokenData.tokenInfo.expiresAt) : undefined,
    accountId: tokenData.accountId ?? tokenData.tokenInfo.chatgptAccountId,
  },
});

const shouldRefreshToken = (tokenData: IOpenAiChatGptTokenData, now: Date): boolean => {
  if (!tokenData.tokenInfo.expiresAt) return false;
  return Date.parse(tokenData.tokenInfo.expiresAt) - now.getTime() < refreshWindowMs;
};

const isExpired = (tokenData: IOpenAiChatGptTokenData, now: Date): boolean => {
  if (!tokenData.tokenInfo.expiresAt) return false;
  return Date.parse(tokenData.tokenInfo.expiresAt) <= now.getTime();
};

const readJsonFileIfExists = async (filePath: string): Promise<Record<string, unknown>> => {
  try {
    const parsed = JSON.parse(await fs.readFile(filePath, 'utf8')) as unknown;
    return isRecord(parsed) ? parsed : {};
  } catch (error) {
    const nodeError = error as NodeJS.ErrnoException;
    if (nodeError.code === 'ENOENT') return {};
    throw error;
  }
};

const writeJsonAtomic = async (filePath: string, payload: unknown): Promise<void> => {
  const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
  await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
  await fs.writeFile(tempPath, `${JSON.stringify(payload, undefined, 2)}\n`, { mode: 0o600 });
  await fs.rename(tempPath, filePath);
};

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

const stringValue = (value: unknown): string | undefined => {
  return typeof value === 'string' && value.length > 0 ? value : undefined;
};
