#!/usr/bin/env node
/**
 * One-command setup: login, deploy, optional permset/data, GraphQL schema/codegen, web app build.
 * Use this script to make setup easier for each app generated from this template.
 *
 * Usage:
 *   node scripts/setup-cli.mjs --target-org <alias>           # interactive step picker (all selected)
 *   node scripts/setup-cli.mjs --target-org <alias> --yes     # skip picker, run all steps
 *   node scripts/setup-cli.mjs --target-org afv5 --skip-login
 *   node scripts/setup-cli.mjs --target-org afv5 --skip-data --skip-webapp-build
 *   node scripts/setup-cli.mjs --target-org myorg --webapp-name my-app
 *
 * Steps (in order):
 *   1. login     — sf org login web only if org not already connected (skip with --skip-login)
 *   2. webapp    — (all web apps) npm install && npm run build so dist exists for deploy (skip with --skip-webapp-build)
 *   3. deploy    — sf project deploy start --target-org <alias> (requires dist for entity deployment)
 *   4. permset   — sf org assign permset (skip with --skip-permset; name via --permset-name)
 *   5. data      — prepare unique fields + sf data import tree (skipped if no data dir/plan)
 *   6. graphql   — (in webapp) npm run graphql:schema then npm run graphql:codegen
 *   7. dev       — (in webapp) npm run dev — launch dev server (skip with --skip-dev)
 */

import { spawnSync } from 'node:child_process';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { readdirSync, existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..');

function resolveSfdxSource() {
  const sfdxPath = resolve(ROOT, 'sfdx-project.json');
  if (!existsSync(sfdxPath)) {
    console.error('Error: sfdx-project.json not found at project root.');
    process.exit(1);
  }
  const sfdxProject = JSON.parse(readFileSync(sfdxPath, 'utf8'));
  const pkgDir = sfdxProject?.packageDirectories?.[0]?.path;
  if (!pkgDir) {
    console.error('Error: No packageDirectories[].path found in sfdx-project.json.');
    process.exit(1);
  }
  return resolve(ROOT, pkgDir, 'main', 'default');
}

const SFDX_SOURCE = resolveSfdxSource();
const WEBAPPLICATIONS_DIR = resolve(SFDX_SOURCE, 'webapplications');
const DATA_DIR = resolve(SFDX_SOURCE, 'data');
const DATA_PLAN = resolve(SFDX_SOURCE, 'data/data-plan.json');

function parseArgs() {
  const args = process.argv.slice(2);
  let targetOrg = null;
  let webappName = null;
  let permsetName = 'Property_Management_Access';
  let yes = false;
  const flags = {
    skipLogin: false,
    skipDeploy: false,
    skipPermset: false,
    skipData: false,
    skipGraphql: false,
    skipWebappBuild: false,
    skipDev: false,
  };
  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--target-org' && args[i + 1]) {
      targetOrg = args[++i];
    } else if (args[i] === '--webapp-name' && args[i + 1]) {
      webappName = args[++i];
    } else if (args[i] === '--permset-name' && args[i + 1]) {
      permsetName = args[++i];
    } else if (args[i] === '--skip-login') flags.skipLogin = true;
    else if (args[i] === '--skip-deploy') flags.skipDeploy = true;
    else if (args[i] === '--skip-permset') flags.skipPermset = true;
    else if (args[i] === '--skip-data') flags.skipData = true;
    else if (args[i] === '--skip-graphql') flags.skipGraphql = true;
    else if (args[i] === '--skip-webapp-build') flags.skipWebappBuild = true;
    else if (args[i] === '--skip-dev') flags.skipDev = true;
    else if (args[i] === '--yes' || args[i] === '-y') yes = true;
    else if (args[i] === '--help' || args[i] === '-h') {
      console.log(`
Setup CLI — one-command setup for apps in this project

Usage:
  node scripts/setup-cli.mjs --target-org <alias> [options]

Required:
  --target-org <alias>   Target Salesforce org alias (e.g. myorg)

Options:
  --webapp-name <name>   Web app folder name under webapplications/ (default: auto-detect)
  --permset-name <name>  Permission set to assign (default: Property_Management_Access)
  --skip-login           Skip login step (login is auto-skipped if org is already connected)
  --skip-deploy          Do not deploy metadata
  --skip-permset         Do not assign permission set
  --skip-data            Do not prepare data or run data import
  --skip-graphql         Do not fetch schema or run GraphQL codegen
  --skip-webapp-build    Do not npm install / build the web application
  --skip-dev             Do not launch the dev server at the end
  -y, --yes              Skip interactive step picker; run all enabled steps immediately
  -h, --help             Show this help
`);
      process.exit(0);
    }
  }
  if (!targetOrg) {
    console.error('Error: --target-org <alias> is required.');
    process.exit(1);
  }
  return { targetOrg, webappName, permsetName, yes, ...flags };
}

function discoverAllWebappDirs(webappName) {
  if (!existsSync(WEBAPPLICATIONS_DIR)) {
    console.error(`Error: webapplications directory not found: ${WEBAPPLICATIONS_DIR}`);
    process.exit(1);
  }
  const entries = readdirSync(WEBAPPLICATIONS_DIR, { withFileTypes: true });
  const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith('.'));
  if (dirs.length === 0) {
    console.error(`Error: No web app folder found under ${WEBAPPLICATIONS_DIR}`);
    process.exit(1);
  }
  if (webappName) {
    const requested = dirs.find((d) => d.name === webappName);
    if (!requested) {
      console.error(`Error: Web app directory not found: ${webappName}`);
      process.exit(1);
    }
    return [resolve(WEBAPPLICATIONS_DIR, requested.name)];
  }
  return dirs.map((d) => resolve(WEBAPPLICATIONS_DIR, d.name));
}

function discoverWebappDir(webappName) {
  const all = discoverAllWebappDirs(webappName);
  if (all.length > 1 && !webappName) {
    console.log(`Multiple web apps found; using first: ${all[0].split(/[/\\]/).pop()}`);
  }
  return all[0];
}

function isOrgConnected(targetOrg) {
  const result = spawnSync('sf', ['org', 'display', '--target-org', targetOrg, '--json'], {
    cwd: ROOT,
    stdio: 'pipe',
    shell: true,
  });
  return result.status === 0;
}

function apexLiteral(value) {
  if (value === null || value === undefined) return 'null';
  if (typeof value === 'boolean') return String(value);
  if (typeof value === 'number') return String(value);
  const s = String(value);
  if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return `Date.valueOf('${s}')`;
  if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(s)) {
    const dt = s.replace('T', ' ').replace(/\.\d+/, '').replace('Z', '');
    return `DateTime.valueOf('${dt}')`;
  }
  return "'" + s.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'";
}

function buildApexInsert(sobject, records, refIds) {
  const lines = [
    'Database.DMLOptions dmlOpts = new Database.DMLOptions();',
    'dmlOpts.DuplicateRuleHeader.allowSave = true;',
    `List<${sobject}> recs = new List<${sobject}>();`,
  ];
  for (const rec of records) {
    lines.push(`{ ${sobject} r = new ${sobject}();`);
    for (const [key, val] of Object.entries(rec)) {
      if (key === 'attributes') continue;
      lines.push(`r.put('${key}', ${apexLiteral(val)});`);
    }
    lines.push('recs.add(r); }');
  }
  lines.push('Database.SaveResult[] results = Database.insert(recs, dmlOpts);');
  const refArray = refIds.map((r) => `'${r}'`).join(',');
  lines.push(`String[] refs = new String[]{${refArray}};`);
  lines.push('for (Integer i = 0; i < results.size(); i++) {');
  lines.push("  if (results[i].isSuccess()) System.debug('REF:' + refs[i] + ':' + results[i].getId());");
  lines.push("  else System.debug('ERR:' + refs[i] + ':' + results[i].getErrors()[0].getMessage());");
  lines.push('}');
  return lines.join('\n');
}

/**
 * Interactive multi-select: arrow keys navigate, space toggles, 'a' toggles all, enter confirms.
 * Returns a boolean[] matching the input order.  Falls through immediately when stdin is not a TTY.
 */
async function promptSteps(steps) {
  if (!process.stdin.isTTY) return steps.map((s) => s.enabled);

  const selected = steps.map((s) => s.enabled);
  let cursor = 0;
  const DIM = '\x1B[2m';
  const RST = '\x1B[0m';
  const CYAN = '\x1B[36m';
  const GREEN = '\x1B[32m';

  function render() {
    return steps.map((s, i) => {
      const ptr = i === cursor ? `${CYAN}❯${RST}` : ' ';
      if (!s.available) return `${ptr} ${DIM}○ ${s.label} (n/a)${RST}`;
      const chk = selected[i] ? `${GREEN}●${RST}` : '○';
      return `${ptr} ${chk} ${s.label}`;
    });
  }

  return new Promise((resolve) => {
    process.stdin.setRawMode(true);
    process.stdin.resume();
    process.stdin.setEncoding('utf8');
    process.stdout.write('\x1B[?25l');
    console.log('\nSelect steps (↑↓ move, space toggle, a all, enter confirm):\n');
    process.stdout.write(render().join('\n') + '\n');

    function redraw() {
      process.stdout.write(`\x1B[${steps.length}A`);
      for (const line of render()) process.stdout.write(`\x1B[2K${line}\n`);
    }

    process.stdin.on('data', (key) => {
      if (key === '\x03') {
        process.stdout.write('\x1B[?25h\n');
        process.exit(0);
      }
      if (key === '\r' || key === '\n') {
        process.stdout.write('\x1B[?25h');
        process.stdin.setRawMode(false);
        process.stdin.pause();
        process.stdin.removeAllListeners('data');
        console.log();
        resolve(selected);
        return;
      }
      if (key === ' ') {
        if (steps[cursor].available) selected[cursor] = !selected[cursor];
        redraw();
        return;
      }
      if (key === 'a') {
        const allOn = steps.every((s, i) => !s.available || selected[i]);
        for (let i = 0; i < steps.length; i++) {
          if (steps[i].available) selected[i] = !allOn;
        }
        redraw();
        return;
      }
      if (key === '\x1B[A' || key === 'k') {
        cursor = Math.max(0, cursor - 1);
        redraw();
      } else if (key === '\x1B[B' || key === 'j') {
        cursor = Math.min(steps.length - 1, cursor + 1);
        redraw();
      }
    });
  });
}

function run(name, cmd, args, opts = {}) {
  const { cwd = ROOT, optional = false } = opts;
  console.log('\n---', name, '---');
  const result = spawnSync(cmd, args, {
    cwd,
    stdio: 'inherit',
    shell: true,
    ...(opts.timeout && { timeout: opts.timeout }),
  });
  if (result.status !== 0 && !optional) {
    console.error(`\nSetup failed at step: ${name}`);
    process.exit(result.status ?? 1);
  }
  return result;
}

async function main() {
  const {
    targetOrg,
    webappName,
    permsetName,
    yes,
    skipLogin: argSkipLogin,
    skipDeploy: argSkipDeploy,
    skipPermset: argSkipPermset,
    skipData: argSkipData,
    skipGraphql: argSkipGraphql,
    skipWebappBuild: argSkipWebappBuild,
    skipDev: argSkipDev,
  } = parseArgs();

  const hasDataPlan = existsSync(DATA_PLAN) && existsSync(DATA_DIR);

  const stepDefs = [
    { key: 'login', label: 'Login — org authentication', enabled: !argSkipLogin, available: true },
    { key: 'webappBuild', label: 'Webapp Build — npm install + build (pre-deploy)', enabled: !argSkipWebappBuild, available: true },
    { key: 'deploy', label: 'Deploy — sf project deploy start', enabled: !argSkipDeploy, available: true },
    { key: 'permset', label: `Permset — assign ${permsetName}`, enabled: !argSkipPermset, available: true },
    { key: 'data', label: 'Data — delete + import records via Apex', enabled: !argSkipData && hasDataPlan, available: hasDataPlan },
    { key: 'graphql', label: 'GraphQL — schema introspect + codegen', enabled: !argSkipGraphql, available: true },
    { key: 'dev', label: 'Dev — launch dev server', enabled: !argSkipDev, available: true },
  ];

  const selections = yes ? stepDefs.map((s) => s.enabled) : await promptSteps(stepDefs);
  const on = {};
  stepDefs.forEach((s, i) => {
    on[s.key] = selections[i];
  });

  const skipLogin = !on.login;
  const skipWebappBuild = !on.webappBuild;
  const skipDeploy = !on.deploy;
  const skipPermset = !on.permset;
  const skipData = !on.data;
  const skipGraphql = !on.graphql;
  const skipDev = !on.dev;

  const needsWebapp = !skipWebappBuild || !skipGraphql || !skipDev;
  const webappDir = needsWebapp ? discoverWebappDir(webappName) : null;
  const doData = !skipData;

  console.log('Setup — target org:', targetOrg, '| web app:', webappDir ?? '(none)');
  console.log(
    'Steps: login=%s deploy=%s permset=%s data=%s graphql=%s webapp=%s dev=%s',
    !skipLogin,
    !skipDeploy,
    !skipPermset,
    doData,
    !skipGraphql,
    !skipWebappBuild,
    !skipDev
  );

  if (!skipLogin) {
    if (isOrgConnected(targetOrg)) {
      console.log('\n--- Login ---');
      console.log(`Org ${targetOrg} is already authenticated; skipping browser login.`);
    } else {
      run('Login (browser)', 'sf', ['org', 'login', 'web', '--alias', targetOrg], { optional: true });
    }
  }

  // Build all web apps before deploy so dist exists for entity deployment
  if (!skipDeploy && !skipWebappBuild) {
    const allWebappDirs = discoverAllWebappDirs(webappName);
    for (const dir of allWebappDirs) {
      const name = dir.split(/[/\\]/).pop();
      run(`Web app install (${name})`, 'npm', ['install'], { cwd: dir });
      run(`Web app build (${name})`, 'npm', ['run', 'build'], { cwd: dir });
    }
  }

  if (!skipDeploy) {
    run('Deploy metadata', 'sf', ['project', 'deploy', 'start', '--target-org', targetOrg], {
      timeout: 180000,
    });
  }

  if (!skipPermset && permsetName) {
    console.log('\n--- Assign permission set ---');
    const permsetResult = spawnSync(
      'sf',
      ['org', 'assign', 'permset', '--name', permsetName, '--target-org', targetOrg],
      {
        cwd: ROOT,
        stdio: 'pipe',
        shell: true,
      }
    );
    if (permsetResult.status === 0) {
      console.log('Permission set assigned.');
    } else {
      const out =
        (permsetResult.stderr?.toString() || '') + (permsetResult.stdout?.toString() || '');
      if (out.includes('Duplicate') && out.includes('PermissionSet')) {
        console.log('Permission set already assigned; skipping.');
      } else if (out.includes('not found') && out.includes('target org')) {
        console.log(`Permission set "${permsetName}" not in org; skipping.`);
      } else {
        process.stdout.write(permsetResult.stdout?.toString() || '');
        process.stderr.write(permsetResult.stderr?.toString() || '');
        console.error('\nSetup failed at step: Assign permission set');
        process.exit(permsetResult.status ?? 1);
      }
    }
  }

  if (doData) {
    // Prepare data for uniqueness (run before import so repeat imports don't conflict)
    const prepareScript = resolve(__dirname, 'prepare-import-unique-fields.js');
    run('Prepare data (unique fields)', 'node', [prepareScript, '--data-dir', DATA_DIR], {
      cwd: ROOT,
    });
    // Normalize Lease__c Tenant refs to 1–15 so all refs resolve (Tenant__c.json has 15 records)
    const leasePath = resolve(DATA_DIR, 'Lease__c.json');
    if (existsSync(leasePath)) {
      let leaseContent = readFileSync(leasePath, 'utf8');
      leaseContent = leaseContent.replace(/@TenantRef(\d+)/g, (_m, n) => {
        const k = ((parseInt(n, 10) - 1) % 15) + 1;
        return `@TenantRef${k}`;
      });
      writeFileSync(leasePath, leaseContent);
    }

    // Delete existing records so every run inserts the full dataset without duplicate conflicts.
    // Reverse plan order ensures children are removed before parents (FK safety).
    console.log('\n--- Clean existing data for fresh import ---');
    const planEntries = JSON.parse(readFileSync(DATA_PLAN, 'utf8'));
    const sobjectsReversed = [...planEntries.map((e) => e.sobject)].reverse();
    const tmpApex = resolve(ROOT, '.tmp-setup-delete.apex');
    for (const sobject of sobjectsReversed) {
      const apexCode = [
        'try {',
        `  List<SObject> recs = Database.query('SELECT Id FROM ${sobject} LIMIT 10000');`,
        '  if (!recs.isEmpty()) {',
        '    Database.delete(recs, false);',
        '    Database.emptyRecycleBin(recs);',
        '  }',
        '} catch (Exception e) {',
        '  // non-deletable records (e.g. Contact linked to Case) are skipped via allOrNone=false',
        '}',
      ].join('\n');
      writeFileSync(tmpApex, apexCode);
      spawnSync('sf', ['apex', 'run', '--target-org', targetOrg, '--file', tmpApex], {
        cwd: ROOT,
        stdio: 'pipe',
        shell: true,
        timeout: 60000,
      });
      console.log(`  ${sobject}: cleaned`);
    }
    if (existsSync(tmpApex)) unlinkSync(tmpApex);

    // Import via Anonymous Apex with Database.DMLOptions.duplicateRuleHeader.allowSave = true.
    // This bypasses both duplicate-rule blocks AND matching-service timeouts that the REST
    // API headers (Sforce-Duplicate-Rule-Action) cannot override.
    console.log('\n--- Data import tree ---');
    const refMap = new Map();
    const APEX_CHAR_LIMIT = 25000;
    const APEX_MAX_BATCH = 200;

    for (const entry of planEntries) {
      for (const file of entry.files) {
        const data = JSON.parse(readFileSync(resolve(DATA_DIR, file), 'utf8'));
        const records = data.records || [];

        for (const rec of records) {
          for (const key of Object.keys(rec)) {
            if (key === 'attributes') continue;
            const val = rec[key];
            if (typeof val === 'string' && val.startsWith('@')) {
              const actual = refMap.get(val.slice(1));
              if (actual) {
                rec[key] = actual;
              } else if (refMap.size > 0) {
                console.warn(`    Warning: unresolved ref ${val} in ${file}`);
              }
            }
          }
        }

        let imported = 0;
        const sampleRec = records[0] || {};
        const fieldsPerRec = Object.keys(sampleRec).filter((k) => k !== 'attributes').length;
        const estCharsPerRec = 40 + fieldsPerRec * 55;
        const batchSize = Math.min(APEX_MAX_BATCH, Math.max(5, Math.floor(APEX_CHAR_LIMIT / estCharsPerRec)));
        for (let i = 0; i < records.length; i += batchSize) {
          const batch = records.slice(i, i + batchSize);
          const refIds = batch.map((r) => r.attributes?.referenceId || `_idx${i}`);
          const apex = buildApexInsert(entry.sobject, batch, refIds);
          writeFileSync(tmpApex, apex);
          const apexResult = spawnSync(
            'sf',
            ['apex', 'run', '--target-org', targetOrg, '--file', tmpApex],
            { cwd: ROOT, stdio: 'pipe', shell: true, timeout: 120000 }
          );
          const apexOut = apexResult.stdout?.toString() || '';
          const apexErr = apexResult.stderr?.toString() || '';
          if (apexResult.status !== 0 && !apexOut.includes('Compiled successfully')) {
            console.error(`  ${entry.sobject}: apex execution failed`);
            process.stderr.write(apexErr || apexOut);
            process.exit(1);
          }
          const okMatches = [...apexOut.matchAll(/\|DEBUG\|REF:([^:\n]+):(\w+)/g)];
          const errMatches = [...apexOut.matchAll(/\|DEBUG\|ERR:([^:\n]+):([^\n]+)/g)];
          if (errMatches.length) {
            for (const m of errMatches.slice(0, 5)) {
              console.error(`    ${m[1]}: ${m[2].trim()}`);
            }
            if (errMatches.length > 5) console.error(`    ... and ${errMatches.length - 5} more`);
            console.error(`\nSetup failed at step: Data import tree (${entry.sobject})`);
            process.exit(1);
          }
          if (entry.saveRefs) {
            for (const m of okMatches) refMap.set(m[1], m[2]);
          }
          imported += okMatches.length;
        }
        console.log(`  ${entry.sobject}: imported ${imported} records`);
      }
    }
    if (existsSync(tmpApex)) unlinkSync(tmpApex);
  }

  if (!skipGraphql || !skipWebappBuild) {
    run('Web app npm install', 'npm', ['install'], { cwd: webappDir });
  }

  if (!skipGraphql) {
    run('Set default org for schema', 'sf', ['config', 'set', 'target-org', targetOrg, '--global']);
    run('GraphQL schema (introspect)', 'npm', ['run', 'graphql:schema'], { cwd: webappDir });
    run('GraphQL codegen', 'npm', ['run', 'graphql:codegen'], { cwd: webappDir });
  }

  if (!skipWebappBuild) {
    run('Web app build', 'npm', ['run', 'build'], { cwd: webappDir });
  }

  console.log('\n--- Setup complete ---');

  if (!skipDev) {
    console.log('\n--- Launching dev server (Ctrl+C to stop) ---\n');
    run('Dev server', 'npm', ['run', 'dev'], { cwd: webappDir });
  }
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
