import { readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
import { resolve } from 'path';
import { homedir } from 'os';

const COPILOT_CLIENT_ID = 'Ov23li8tweQw6odWQebz';
const DEVICE_CODE_URL = 'https://github.com/login/device/code';
const TOKEN_URL = 'https://github.com/login/oauth/access_token';
const POLLING_SAFETY_MARGIN_MS = 3000;

const AUTH_DIR = resolve(homedir(), '.config/flynn');
const AUTH_FILE = resolve(AUTH_DIR, 'auth.json');

export interface DeviceCodeResponse {
  device_code: string;
  user_code: string;
  verification_uri: string;
  expires_in: number;
  interval: number;
}

interface AuthStore {
  github?: {
    access_token: string;
    created_at: string;
  };
}

/**
 * Request a device code from GitHub to start the OAuth device flow.
 */
export async function requestDeviceCode(): Promise<DeviceCodeResponse> {
  const response = await fetch(DEVICE_CODE_URL, {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ client_id: COPILOT_CLIENT_ID }),
  });

  if (!response.ok) {
    throw new Error(`Failed to request device code: ${response.status} ${response.statusText}`);
  }

  return response.json() as Promise<DeviceCodeResponse>;
}

/**
 * Poll GitHub for an access token after the user has entered the device code.
 * Blocks until the user authorizes or the code expires.
 */
export async function pollForToken(deviceCode: string, interval: number): Promise<string> {
  let currentInterval = interval;

  while (true) {
    await new Promise(r => setTimeout(r, currentInterval * 1000 + POLLING_SAFETY_MARGIN_MS));

    const response = await fetch(TOKEN_URL, {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        client_id: COPILOT_CLIENT_ID,
        device_code: deviceCode,
        grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
      }),
    });

    const data = await response.json() as Record<string, unknown>;

    if (data.access_token) {
      return data.access_token as string;
    }

    if (data.error === 'authorization_pending') {
      continue;
    }

    if (data.error === 'slow_down') {
      // Add 5 seconds as per GitHub spec
      currentInterval = (data.interval as number) ?? currentInterval + 5;
      continue;
    }

    if (data.error === 'expired_token') {
      throw new Error('Device code expired. Please try again.');
    }

    if (data.error === 'access_denied') {
      throw new Error('Authorization was denied by the user.');
    }

    throw new Error(`OAuth error: ${data.error ?? 'unknown'} - ${data.error_description ?? ''}`);
  }
}

/**
 * Load a previously stored GitHub OAuth token from disk.
 * Returns null if no token is stored or the file doesn't exist.
 */
export function loadStoredToken(): string | null {
  try {
    const raw = readFileSync(AUTH_FILE, 'utf-8');
    const store = JSON.parse(raw) as AuthStore;
    return store.github?.access_token ?? null;
  } catch {
    return null;
  }
}

/**
 * Store a GitHub OAuth token to disk with secure permissions.
 */
export function storeToken(token: string): void {
  mkdirSync(AUTH_DIR, { recursive: true });

  let store: AuthStore = {};
  try {
    const raw = readFileSync(AUTH_FILE, 'utf-8');
    store = JSON.parse(raw) as AuthStore;
  } catch {
    // File doesn't exist yet — start fresh
  }

  store.github = {
    access_token: token,
    created_at: new Date().toISOString(),
  };

  writeFileSync(AUTH_FILE, JSON.stringify(store, null, 2) + '\n', 'utf-8');
  chmodSync(AUTH_FILE, 0o600);
}

/**
 * Get a GitHub token from any available source.
 * Priority: GITHUB_TOKEN env var → stored OAuth token → null
 */
export function getGitHubToken(): string | null {
  // 1. Environment variable
  const envToken = process.env.GITHUB_TOKEN;
  if (envToken) {return envToken;}

  // 2. Stored OAuth token
  return loadStoredToken();
}

/**
 * Run the full GitHub OAuth device flow interactively.
 * @param onPrompt Callback to display the user code and verification URL to the user.
 * @returns The access token.
 */
export async function loginGitHub(
  onPrompt: (userCode: string, verificationUri: string) => void,
): Promise<string> {
  const deviceCode = await requestDeviceCode();

  onPrompt(deviceCode.user_code, deviceCode.verification_uri);

  const token = await pollForToken(deviceCode.device_code, deviceCode.interval);
  storeToken(token);
  return token;
}
