/**
 * deactivate-cis-for-inactive-companies.ts
 *
 * Finds all active Configuration Items whose parent company is inactive in
 * Autotask and deactivates them (sets isActive = false).
 *
 * Usage:
 *   npx tsx scripts/deactivate-cis-for-inactive-companies.ts [options]
 *
 * Options:
 *   --dry-run          Preview changes without writing (default)
 *   --commit           Apply changes
 *   --concurrency <n>  Parallel PATCH calls (default: 5)
 */

import { config } from 'dotenv';
import { resolve } from 'path';

config({ path: resolve(__dirname, '../.env') });

// ─── Config ───────────────────────────────────────────────────────────────────

const API_BASE = process.env.AUTOTASK_API_URL!;
const USERNAME = process.env.AUTOTASK_USERNAME!;
const SECRET   = process.env.AUTOTASK_SECRET!;
const INT_CODE = process.env.AUTOTASK_API_INTEGRATION_CODE!;

// ─── Helpers ──────────────────────────────────────────────────────────────────

function authHeaders(): Record<string, string> {
  return {
    'Username':           USERNAME,
    'Secret':             SECRET,
    'APIIntegrationcode': INT_CODE,
    'Content-Type':       'application/json',
    'Accept':             'application/json',
  };
}

/** Paginate through all results of a POST /query endpoint */
async function queryAll<T>(entity: string, filter: object[]): Promise<T[]> {
  const all: T[] = [];
  let nextUrl: string | null = null;
  const requestBody = JSON.stringify({ MaxRecords: 500, filter });

  while (true) {
    const url = nextUrl ?? `${API_BASE}/${entity}/query`;
    const res = await fetch(url, {
      method:  'POST',
      headers: authHeaders(),
      body:    requestBody,
    });
    if (!res.ok) throw new Error(`POST ${entity}/query → ${res.status}: ${await res.text()}`);
    const data: any = await res.json();
    all.push(...(data.items ?? []));
    nextUrl = data.pageDetails?.nextPageUrl ?? null;
    if (!nextUrl) break;
  }

  return all;
}

async function patchCI(id: number, patch: object, retries = 5): Promise<void> {
  for (let attempt = 0; attempt <= retries; attempt++) {
    const res = await fetch(`${API_BASE}/ConfigurationItems`, {
      method:  'PATCH',
      headers: authHeaders(),
      body:    JSON.stringify({ id, ...patch }),
    });
    if (res.ok) return;
    if (res.status === 429 && attempt < retries) {
      const delay = 2000 * (attempt + 1); // 2s, 4s, 6s, 8s, 10s
      await new Promise(r => setTimeout(r, delay));
      continue;
    }
    throw new Error(`PATCH ConfigurationItems/${id} → ${res.status}: ${await res.text()}`);
  }
}

/** Run tasks with a concurrency cap, returns results in original order */
async function pLimit<T>(
  tasks: (() => Promise<T>)[],
  concurrency: number
): Promise<{ result?: T; error?: Error }[]> {
  const results: { result?: T; error?: Error }[] = new Array(tasks.length);
  let next = 0;
  async function worker() {
    while (next < tasks.length) {
      const i = next++;
      try { results[i] = { result: await tasks[i]() }; }
      catch (e) { results[i] = { error: e instanceof Error ? e : new Error(String(e)) }; }
    }
  }
  await Promise.all(Array.from({ length: Math.min(concurrency, tasks.length) }, worker));
  return results;
}

// ─── Arg parsing ──────────────────────────────────────────────────────────────

function parseArgs() {
  const args = process.argv.slice(2);
  let dryRun      = true;
  let concurrency = 3; // Autotask hard limit is 3 concurrent API threads
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--commit')                   dryRun = false;
    if (args[i] === '--dry-run')                  dryRun = true;
    if (args[i] === '--concurrency' && args[i+1]) concurrency = parseInt(args[++i], 10);
  }
  return { dryRun, concurrency };
}

// ─── Types ────────────────────────────────────────────────────────────────────

interface Company {
  id: number;
  companyName: string;
  isActive: boolean;
}

interface CIItem {
  id: number;
  companyID: number;
  referenceTitle: string;
  configurationItemType: number;
  configurationItemCategoryID: number | null;
  isActive: boolean;
}

// ─── Main ─────────────────────────────────────────────────────────────────────

async function main() {
  const { dryRun, concurrency } = parseArgs();

  console.log('');
  console.log('╔══════════════════════════════════════════════════════════════╗');
  console.log('║       Deactivate CIs for Inactive Companies                 ║');
  console.log('╚══════════════════════════════════════════════════════════════╝');
  console.log(`  Mode : ${dryRun ? '🔍 DRY RUN (no changes written)' : '✏️  COMMIT (changes will be applied)'}`);
  console.log('');

  // ── 1. Fetch all inactive companies ─────────────────────────────────────────
  process.stdout.write('Fetching inactive companies … ');
  const inactiveCompanies = await queryAll<Company>('Companies', [
    { field: 'isActive', op: 'eq', value: false },
  ]);
  console.log(`found ${inactiveCompanies.length}.`);

  if (inactiveCompanies.length === 0) {
    console.log('No inactive companies found.');
    return;
  }

  const companyMap = new Map<number, string>(
    inactiveCompanies.map(c => [c.id, c.companyName])
  );

  // ── 2. Fetch active CIs for each inactive company ────────────────────────────
  console.log('Querying active CIs for each inactive company …');

  const cisByCompany = new Map<number, CIItem[]>();
  let totalFound = 0;

  for (const company of inactiveCompanies) {
    const cis = await queryAll<CIItem>('ConfigurationItems', [
      { field: 'companyID', op: 'eq', value: company.id },
      { field: 'isActive',  op: 'eq', value: true },
    ]);
    if (cis.length > 0) {
      cisByCompany.set(company.id, cis);
      totalFound += cis.length;
      process.stdout.write(`  [${company.id}] ${company.companyName}: ${cis.length} active CI(s)\n`);
    }
  }

  // ── 3. Report ────────────────────────────────────────────────────────────────
  console.log('');
  console.log('─── Summary ──────────────────────────────────────────────────');
  console.log(`  Inactive companies checked       : ${inactiveCompanies.length}`);
  console.log(`  Companies with active CIs        : ${cisByCompany.size}`);
  console.log(`  Total active CIs to deactivate   : ${totalFound}`);
  console.log('');

  if (totalFound === 0) {
    console.log('✓ Nothing to do — no active CIs found for inactive companies.');
    return;
  }

  console.log('─── Breakdown ────────────────────────────────────────────────');
  const sorted = [...cisByCompany.entries()].sort((a, b) =>
    (companyMap.get(a[0]) ?? '').localeCompare(companyMap.get(b[0]) ?? '')
  );
  for (const [companyId, cis] of sorted) {
    const name = companyMap.get(companyId) ?? `Company ${companyId}`;
    console.log(`\n  ${name} (${cis.length} CI${cis.length !== 1 ? 's' : ''})`);
    for (const ci of cis) {
      console.log(`    [${ci.id}] ${(ci.referenceTitle ?? '(no title)').substring(0, 60)}`);
    }
  }
  console.log('');

  if (dryRun) {
    console.log('─── Dry run complete ─────────────────────────────────────────');
    console.log(`  ${totalFound} CI(s) across ${cisByCompany.size} company/companies would be deactivated.`);
    console.log('  Run with --commit to apply.');
    return;
  }

  // ── 4. Deactivate ────────────────────────────────────────────────────────────
  const allCIs = [...cisByCompany.values()].flat();
  console.log(`─── Deactivating ${allCIs.length} CIs (concurrency=${concurrency}) ──────`);

  const tasks = allCIs.map(ci => () => patchCI(ci.id, { isActive: false }));
  const results = await pLimit(tasks, concurrency);

  let succeeded = 0;
  let failed    = 0;
  const errors: { ci: CIItem; error: string }[] = [];

  for (let i = 0; i < results.length; i++) {
    if (results[i].error) {
      failed++;
      errors.push({ ci: allCIs[i], error: results[i].error!.message });
      process.stdout.write('✗');
    } else {
      succeeded++;
      process.stdout.write('.');
    }
    if ((i + 1) % 80 === 0) process.stdout.write('\n');
  }
  console.log('\n');

  // ── 5. Final report ───────────────────────────────────────────────────────────
  console.log('─── Results ──────────────────────────────────────────────────');
  console.log(`  ✓ Deactivated : ${succeeded}`);
  console.log(`  ✗ Failed      : ${failed}`);

  if (errors.length > 0) {
    console.log('\n  Failures:');
    for (const { ci, error } of errors) {
      const name = companyMap.get(ci.companyID) ?? `Company ${ci.companyID}`;
      console.log(`    [${ci.id}] ${ci.referenceTitle ?? '(no title)'} (${name})`);
      console.log(`      ${error}`);
    }
    process.exit(1);
  }

  console.log('\n✓ All done.');
}

main().catch(err => {
  console.error('\nFatal error:', err.message);
  process.exit(1);
});
