/**
 * Duo Security API Client
 * Handles HMAC-SHA1 request signing, pagination, rate-limit handling.
 * Supports both the Accounts API (parent) and Admin API (parent + child accounts).
 */

import crypto from 'crypto';
import https from 'https';

export interface DuoAccount {
  account_id: string;
  api_hostname: string;
  name: string;
}

export interface DuoClientOptions {
  ikey: string;
  skey: string;
  host: string;
  timeoutMs?: number;
}

export interface DuoApiResponse<T = any> {
  stat: string;
  response: T;
  metadata?: { total_objects?: number; next_offset?: number; };
}

export interface DuoUserSummary {
  user_count: number;
  integration_count: number;
  telephony_credits_remaining?: number;
}

export interface DuoUser {
  user_id: string;
  username: string;
  email: string;
  realname: string;
  status: string;
  is_enrolled: boolean;
  last_login: number | null;
  last_directory_sync: number | null;
  created: number;
  notes: string;
  phones: any[];
  groups: any[];
  aliases: Record<string, string | null>;
  enable_auto_prompt: boolean;
  firstname: string;
  lastname: string;
}

export interface DuoPhone {
  phone_id: string;
  name: string;
  number: string;
  type: string;
  platform: string;
  model: string;
  os_version: string;
  app_version: string;
  activated: boolean;
  last_seen: string;
  capabilities: string[];
  users: any[];
}

export interface DuoGroup {
  group_id: string;
  name: string;
  desc: string;
  status: string;
  mobile_otp_enabled: boolean;
  push_enabled: boolean;
  sms_enabled: boolean;
  voice_enabled: boolean;
}

export interface DuoIntegration {
  integration_key: string;
  name: string;
  type: string;
  notes: string;
}

export interface DuoAuthLog {
  txid: string;
  timestamp: number;
  user: { key: string; name: string; };
  factor: string;
  result: string;
  reason: string;
  application: { key: string; name: string; };
  access_device: { ip: string; hostname: string | null; location: { city: string; state: string; country: string; }; };
  auth_device: { ip: string | null; name: string; };
  event_type: string;
}

export class DuoClient {
  private readonly ikey: string;
  private readonly skey: string;
  private readonly host: string;
  private readonly timeoutMs: number;

  constructor(options: DuoClientOptions) {
    if (!options.ikey || !options.skey || !options.host) {
      throw new Error('DuoClient requires ikey, skey, and host');
    }
    this.ikey = options.ikey;
    this.skey = options.skey;
    this.host = options.host;
    this.timeoutMs = options.timeoutMs ?? 30_000;
  }

  // ── 1.2  HMAC-SHA1 request signing ────────────────────────────────────────

  private sign(method: string, host: string, path: string, params: Record<string, string>, date: string): string {
    const sortedParams = Object.keys(params).sort()
      .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
      .join('&');
    const canon = [date, method.toUpperCase(), host.toLowerCase(), path, sortedParams].join('\n');
    const sig = crypto.createHmac('sha1', this.skey).update(canon).digest('hex');
    return `Basic ${Buffer.from(`${this.ikey}:${sig}`).toString('base64')}`;
  }

  // ── 1.3  Core request methods (with 1.5 rate-limit + 1.6 timeout) ────────

  private request<T = any>(
    method: string,
    path: string,
    params: Record<string, string> = {},
    hostOverride?: string,
  ): Promise<DuoApiResponse<T>> {
    const host = hostOverride ?? this.host;
    const date = new Date().toUTCString();
    const auth = this.sign(method, host, path, params, date);

    const sortedParams = Object.keys(params).sort()
      .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
      .join('&');

    const isGet = method.toUpperCase() === 'GET';
    const urlPath = isGet ? `${path}${sortedParams ? '?' + sortedParams : ''}` : path;
    const body = isGet ? '' : sortedParams;

    return new Promise((resolve, reject) => {
      const req = https.request({
        hostname: host,
        path: urlPath,
        method: method.toUpperCase(),
        headers: {
          Authorization: auth,
          Date: date,
          'Content-Type': 'application/x-www-form-urlencoded',
          ...(body ? { 'Content-Length': String(Buffer.byteLength(body)) } : {}),
        },
      }, res => {
        let data = '';
        res.on('data', (chunk: Buffer) => data += chunk);
        res.on('end', async () => {
          // 1.5 rate-limit handling
          if (res.statusCode === 429) {
            const retryAfter = parseInt(res.headers['retry-after'] as string || '5', 10);
            console.log(`[DuoClient] Rate limited on ${path}, retrying in ${retryAfter}s`);
            await new Promise(r => setTimeout(r, retryAfter * 1000));
            return resolve(this.request<T>(method, path, params, hostOverride));
          }
          try {
            const parsed = JSON.parse(data) as DuoApiResponse<T>;
            if (parsed.stat !== 'OK') {
              reject(new Error(`Duo API error on ${path}: ${(parsed as any).message || JSON.stringify(parsed)}`));
            } else {
              resolve(parsed);
            }
          } catch {
            reject(new Error(`Duo API non-JSON response on ${path}: HTTP ${res.statusCode} — ${data.slice(0, 200)}`));
          }
        });
      });

      // 1.6 timeout
      req.setTimeout(this.timeoutMs, () => {
        req.destroy();
        reject(new Error(`Duo API timeout on ${path} after ${this.timeoutMs}ms`));
      });
      req.on('error', reject);
      if (body) req.write(body);
      req.end();
    });
  }

  async get<T = any>(path: string, params: Record<string, string> = {}, hostOverride?: string): Promise<DuoApiResponse<T>> {
    return this.request<T>('GET', path, params, hostOverride);
  }

  async post<T = any>(path: string, params: Record<string, string> = {}, hostOverride?: string): Promise<DuoApiResponse<T>> {
    return this.request<T>('POST', path, params, hostOverride);
  }

  // ── 1.4  Paginated GET — follows metadata.next_offset ─────────────────────

  async getPaginated<T = any>(path: string, params: Record<string, string> = {}, hostOverride?: string): Promise<T[]> {
    const all: T[] = [];
    let offset = 0;
    const limit = '300';

    while (true) {
      const pageParams = { ...params, limit, offset: String(offset) };
      const res = await this.get<T[]>(path, pageParams, hostOverride);
      const items = res.response;
      if (!Array.isArray(items)) break;
      all.push(...items);

      if (res.metadata?.next_offset !== undefined && res.metadata.next_offset !== null) {
        offset = res.metadata.next_offset;
      } else {
        break;
      }
    }
    return all;
  }

  // ── 1.7  Accounts API ─────────────────────────────────────────────────────

  async listAccounts(): Promise<DuoAccount[]> {
    const res = await this.post<DuoAccount[]>('/accounts/v1/account/list');
    return res.response;
  }

  // ── 1.8 + 1.9  Admin API methods (with child account support) ─────────────
  //   When `child` is provided, requests are signed against child.api_hostname
  //   and account_id is passed as a parameter.

  private childParams(child?: DuoAccount): Record<string, string> {
    return child ? { account_id: child.account_id } : {};
  }

  private childHost(child?: DuoAccount): string | undefined {
    return child?.api_hostname;
  }

  async getAccountSummary(child?: DuoAccount): Promise<DuoUserSummary> {
    const res = await this.get<DuoUserSummary>(
      '/admin/v1/info/summary',
      this.childParams(child),
      this.childHost(child),
    );
    return res.response;
  }

  async getUsers(child?: DuoAccount): Promise<DuoUser[]> {
    return this.getPaginated<DuoUser>(
      '/admin/v1/users',
      this.childParams(child),
      this.childHost(child),
    );
  }

  async getPhones(child?: DuoAccount): Promise<DuoPhone[]> {
    return this.getPaginated<DuoPhone>(
      '/admin/v1/phones',
      this.childParams(child),
      this.childHost(child),
    );
  }

  async getGroups(child?: DuoAccount): Promise<DuoGroup[]> {
    return this.getPaginated<DuoGroup>(
      '/admin/v1/groups',
      this.childParams(child),
      this.childHost(child),
    );
  }

  async getIntegrations(child?: DuoAccount): Promise<DuoIntegration[]> {
    return this.getPaginated<DuoIntegration>(
      '/admin/v1/integrations',
      this.childParams(child),
      this.childHost(child),
    );
  }

  async getAuthLogs(mintime: number, child?: DuoAccount): Promise<DuoAuthLog[]> {
    // Auth logs v2 has a different response structure:
    //   { stat: "OK", response: { authlogs: [...], metadata: { next_offset: [...] } } }
    // So we can't use getPaginated — custom pagination here.
    const all: DuoAuthLog[] = [];
    let nextOffset: string[] | undefined;

    while (true) {
      const params: Record<string, string> = {
        ...this.childParams(child),
        mintime: String(mintime),
        maxtime: String(Date.now()),
        limit: '1000',
      };
      if (nextOffset) {
        params.next_offset = JSON.stringify(nextOffset);
      }

      const res = await this.get<{ authlogs: DuoAuthLog[]; metadata: { next_offset?: string[] } }>(
        '/admin/v2/logs/authentication',
        params,
        this.childHost(child),
      );

      const logs = res.response?.authlogs ?? [];
      all.push(...logs);

      nextOffset = res.response?.metadata?.next_offset;
      if (!nextOffset || logs.length === 0) break;
    }
    return all;
  }
}

// ── Factory helpers ──────────────────────────────────────────────────────────

export function getDuoAccountsClient(): DuoClient {
  return new DuoClient({
    ikey: process.env.DUOACCOUNTS_INTEGRATION_KEY!,
    skey: process.env.DUOACCOUNTS_SECRET_KEY!,
    host: process.env.DUOACCOUNTS_API_HOSTNAME!,
  });
}

export function getDuoAdminClient(): DuoClient {
  return new DuoClient({
    ikey: process.env.DUOADMIN_INTEGRATION_KEY!,
    skey: process.env.DUOADMIN_SECRET_KEY!,
    host: process.env.DUOADMIN_API_HOSTNAME!,
  });
}
