/**
 * update-workstation-desktop-category.ts
 *
 * Finds all active Configuration Items with type "Workstation – Desktop" (typeId=40)
 * and sets their category to "Workstations".
 *
 * Usage:
 *   npx tsx scripts/update-workstation-desktop-category.ts [options]
 *
 * Options:
 *   --dry-run              Preview changes without writing to Autotask (default: true)
 *   --commit               Actually apply changes (disables dry-run)
 *   --company <name|id>    Filter to a single company (partial name match or numeric ID)
 *   --concurrency <n>      Parallel API update calls (default: 5)
 */

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

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

// ─── Constants ───────────────────────────────────────────────────────────────

const API_BASE   = process.env.AUTOTASK_API_URL!;        // e.g. https://webservices1.autotask.net/atservicesrest/v1.0
const USERNAME   = process.env.AUTOTASK_USERNAME!;
const SECRET     = process.env.AUTOTASK_SECRET!;
const INT_CODE   = process.env.AUTOTASK_API_INTEGRATION_CODE!;

const CI_TYPE_WORKSTATION_DESKTOP = 40;   // "Workstation – Desktop" picklist value
const TARGET_CATEGORY_NAME        = 'Workstations';

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

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

async function apiGet<T>(path: string): Promise<T> {
  const res = await fetch(`${API_BASE}/${path}`, {
    method: 'GET',
    headers: authHeaders(),
  });
  if (!res.ok) throw new Error(`GET ${path} → ${res.status}: ${await res.text()}`);
  return res.json();
}

async function apiPost<T>(path: string, body: object): Promise<T> {
  const res = await fetch(`${API_BASE}/${path}`, {
    method: 'POST',
    headers: authHeaders(),
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`POST ${path} → ${res.status}: ${await res.text()}`);
  return res.json();
}

async function apiPatch(path: string, body: object, retries = 5): Promise<void> {
  for (let attempt = 0; attempt <= retries; attempt++) {
    const res = await fetch(`${API_BASE}/${path}`, {
      method: 'PATCH',
      headers: authHeaders(),
      body: JSON.stringify(body),
    });
    if (res.ok) return;
    if (res.status === 429 && attempt < retries) {
      await new Promise(r => setTimeout(r, 2000 * (attempt + 1)));
      continue;
    }
    throw new Error(`PATCH ${path} → ${res.status}: ${await res.text()}`);
  }
}

/** 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;
  // Autotask requires a body on every POST page request — send same body for all pages
  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;
}

/** Run N promises with a concurrency cap */
async function pLimit<T>(tasks: (() => Promise<T>)[], concurrency: number): Promise<{ result?: T; error?: Error; index: number }[]> {
  const results: { result?: T; error?: Error; index: number }[] = new Array(tasks.length);
  let next = 0;

  async function worker() {
    while (next < tasks.length) {
      const i = next++;
      try {
        results[i] = { result: await tasks[i](), index: i };
      } catch (e) {
        results[i] = { error: e instanceof Error ? e : new Error(String(e)), index: i };
      }
    }
  }

  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 companyArg  = '';
  let concurrency = 3;

  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--commit')                  dryRun = false;
    if (args[i] === '--dry-run')                 dryRun = true;
    if (args[i] === '--company' && args[i + 1])  { companyArg = args[++i]; }
    if (args[i] === '--concurrency' && args[i+1]) { concurrency = parseInt(args[++i], 10); }
  }

  return { dryRun, companyArg, concurrency };
}

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

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

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

interface Category {
  id: number;
  name: string;
  isActive: boolean;
}

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

  console.log('');
  console.log('╔══════════════════════════════════════════════════════════════╗');
  console.log('║   Workstation-Desktop → Workstations Category Remediation   ║');
  console.log('╚══════════════════════════════════════════════════════════════╝');
  console.log(`  Mode        : ${dryRun ? '🔍 DRY RUN (no changes written)' : '✏️  COMMIT (changes will be applied)'}`);
  if (companyArg) console.log(`  Client filter: "${companyArg}"`);
  console.log('');

  // ── 1. Resolve target category ID ─────────────────────────────────────────
  process.stdout.write('Fetching ConfigurationItemCategories … ');
  const categories = await queryAll<Category>('ConfigurationItemCategories', [
    { field: 'isActive', op: 'eq', value: true },
  ]);
  const targetCategory = categories.find(c =>
    c.name.toLowerCase() === TARGET_CATEGORY_NAME.toLowerCase()
  );
  if (!targetCategory) {
    console.error(`\n✗ Category "${TARGET_CATEGORY_NAME}" not found in Autotask. Available:\n`);
    categories.forEach(c => console.error(`  [${c.id}] ${c.name}`));
    process.exit(1);
  }
  console.log(`found ${categories.length} categories.`);
  console.log(`  → Target category: [${targetCategory.id}] "${targetCategory.name}"`);

  // ── 2. Resolve company filter ──────────────────────────────────────────────
  let companyIdFilter: number | null = null;
  let companyName = '';

  if (companyArg) {
    process.stdout.write(`Resolving company "${companyArg}" … `);
    const numericId = parseInt(companyArg, 10);

    if (!isNaN(numericId)) {
      // Direct ID
      const resp: any = await apiGet(`Companies/${numericId}`);
      const co: Company = resp.item;
      if (!co) { console.error(`\n✗ Company ID ${numericId} not found.`); process.exit(1); }
      companyIdFilter = co.id;
      companyName     = co.companyName;
    } else {
      // Name search — fetch all, filter locally (Autotask doesn't support contains on companyName efficiently)
      const companies = await queryAll<Company>('Companies', [
        { field: 'isActive', op: 'eq', value: true },
      ]);
      const matches = companies.filter(c =>
        c.companyName.toLowerCase().includes(companyArg.toLowerCase())
      );
      if (matches.length === 0) {
        console.error(`\n✗ No active company matching "${companyArg}".`); process.exit(1);
      }
      if (matches.length > 1) {
        console.error(`\n✗ "${companyArg}" matched ${matches.length} companies — be more specific:`);
        matches.forEach(c => console.error(`  [${c.id}] ${c.companyName}`));
        process.exit(1);
      }
      companyIdFilter = matches[0].id;
      companyName     = matches[0].companyName;
    }
    console.log(`resolved → [${companyIdFilter}] ${companyName}`);
  }

  // ── 3. Fetch matching CIs from Autotask ────────────────────────────────────
  process.stdout.write('Querying ConfigurationItems (type=Workstation-Desktop, isActive=true) … ');

  // Note: configurationItemType is not queryable via API filter — fetch by isActive
  // (+ optional companyID) then filter by type client-side.
  const ciFilter: object[] = [
    { field: 'isActive', op: 'eq', value: true },
  ];
  if (companyIdFilter !== null) {
    ciFilter.push({ field: 'companyID', op: 'eq', value: companyIdFilter });
  }

  const rawCIs = await queryAll<CIItem>('ConfigurationItems', ciFilter);
  const allCIs = rawCIs.filter(ci => ci.configurationItemType === CI_TYPE_WORKSTATION_DESKTOP);
  console.log(`found ${rawCIs.length} active items, ${allCIs.length} are type "Workstation – Desktop".`);

  // ── 4. Identify items that actually need updating ──────────────────────────
  const toUpdate = allCIs.filter(ci => ci.configurationItemCategoryID !== targetCategory.id);
  const alreadyCorrect = allCIs.length - toUpdate.length;

  // ── 5. Build per-company summary ──────────────────────────────────────────
  const byCompany = new Map<number, { companyID: number; items: CIItem[] }>();
  for (const ci of toUpdate) {
    if (!byCompany.has(ci.companyID)) byCompany.set(ci.companyID, { companyID: ci.companyID, items: [] });
    byCompany.get(ci.companyID)!.items.push(ci);
  }

  // Fetch company names for the affected companies
  const companyNames = new Map<number, string>();
  if (byCompany.size > 0) {
    process.stdout.write(`Fetching company names for ${byCompany.size} affected companies … `);
    const companyIds = [...byCompany.keys()];
    // Batch in groups of 50 (Autotask OR filter limit)
    const chunkSize = 50;
    for (let i = 0; i < companyIds.length; i += chunkSize) {
      const chunk = companyIds.slice(i, i + chunkSize);
      const filter = chunk.map(id => ({ field: 'id', op: 'eq', value: id }));
      const cos = await queryAll<Company>('Companies', filter);
      for (const co of cos) companyNames.set(co.id, co.companyName);
    }
    console.log('done.');
  }

  // ── 6. Report ──────────────────────────────────────────────────────────────
  console.log('');
  console.log('─── Summary ──────────────────────────────────────────────────');
  console.log(`  Total "Workstation – Desktop" CIs found : ${allCIs.length}`);
  console.log(`  Already categorised as "Workstations"   : ${alreadyCorrect}`);
  console.log(`  Require update                          : ${toUpdate.length}`);
  console.log('');

  if (toUpdate.length === 0) {
    console.log('✓ Nothing to do — all items already have the correct category.');
    return;
  }

  console.log('─── Breakdown by client ──────────────────────────────────────');
  const sortedCompanies = [...byCompany.values()].sort((a, b) =>
    (companyNames.get(a.companyID) ?? '').localeCompare(companyNames.get(b.companyID) ?? '')
  );
  for (const { companyID, items } of sortedCompanies) {
    const name = companyNames.get(companyID) ?? `Company ${companyID}`;
    console.log(`  ${name.padEnd(45)} ${items.length} item(s)`);
    for (const ci of items) {
      const oldCat = ci.configurationItemCategoryID ?? 'none';
      console.log(`    [${ci.id}] ${(ci.referenceTitle ?? '(no title)').substring(0, 60)}  (cat: ${oldCat} → ${targetCategory.id})`);
    }
  }
  console.log('');

  if (dryRun) {
    console.log('─── Dry run complete ─────────────────────────────────────────');
    console.log(`  ${toUpdate.length} item(s) would be updated.`);
    console.log('  Run with --commit to apply changes.');
    return;
  }

  // ── 7. Apply updates ───────────────────────────────────────────────────────
  console.log(`─── Applying ${toUpdate.length} updates (concurrency=${concurrency}) ────────`);

  let succeeded = 0;
  let failed    = 0;
  const errors: { id: number; title: string; error: string }[] = [];

  const tasks = toUpdate.map(ci => async () => {
    await apiPatch('ConfigurationItems', {
      id: ci.id,
      configurationItemCategoryID: targetCategory.id,
    });
    return ci.id;
  });

  const results = await pLimit(tasks, concurrency);

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

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

  if (errors.length > 0) {
    console.log('');
    console.log('  Failures:');
    for (const e of errors) {
      console.log(`    [${e.id}] ${e.title}`);
      console.log(`      ${e.error}`);
    }
  }

  console.log('');
  if (failed === 0) {
    console.log('✓ All done.');
  } else {
    console.log('⚠  Completed with errors — review failures above.');
    process.exit(1);
  }
}

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