// chart:load — thin CLI wrapper. Parses args, reads the xlsx, hands off to
// tools/lib/chart-load.ts for every DB-flow step, then prints the SAME
// output format as before the refactor (docs/superpowers/plans/
// 2026-07-31-local-app.md, Task 2). Admin (superuser) connection: this is
// the one loader step allowed to create companies and users.
// See also docs/superpowers/plans/2026-07-30-qb-migration-loader.md, Task 1.

import { parseArgs } from "node:util";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Client } from "pg";
import "dotenv/config";
import { resolveDatabaseUrls } from "../server/src/db.js";
import { readQbSheet } from "./qb-xlsx.js";
import { planAccounts, findDuplicateNames } from "./chart-logic.js";
import { ensureCompanyAndOwner, loadChart, parseListingRows } from "./lib/chart-load.js";

// Resolved when it is needed, not at import: the same resolution the app uses
// (DATABASE_URL, then the bundled instance.json). The hardcoded
// `postgres:postgres@localhost:5432/accounting` that used to be here stopped
// being safe when Docker was removed on 2026-08-08 — the bundled PostgreSQL
// listens on 54329 specifically so it cannot collide with a developer's own,
// which means port 5432 is now, by design, somebody else's database. This tool
// writes a chart of accounts into whatever it is pointed at.
async function databaseUrl(): Promise<string> {
  return (await resolveDatabaseUrls()).admin;
}

interface Args {
  file: string;
  company: string;
  ownerEmail: string;
  ownerName: string;
}

function parseCliArgs(argv: string[]): Args {
  const { values } = parseArgs({
    args: argv,
    options: {
      file: { type: "string" },
      company: { type: "string" },
      "owner-email": { type: "string", default: "owner@local" },
      "owner-name": { type: "string", default: "Owner" },
    },
  });
  if (!values.file || !values.company) {
    throw new Error(
      "usage: chart:load --file <xlsx> --company <name> " +
        "[--owner-email <email>] [--owner-name <display name>]",
    );
  }
  return {
    file: values.file,
    company: values.company,
    ownerEmail: values["owner-email"]!,
    ownerName: values["owner-name"]!,
  };
}

export async function run(argv: string[]): Promise<number> {
  const args = parseCliArgs(argv);

  // Read and parse the file BEFORE touching the database — a bad file
  // should never create a company/user as a side effect.
  const sheetRows = await readQbSheet(args.file);
  const listingRows = parseListingRows(sheetRows);
  if (listingRows.length === 0) {
    console.error(`empty listing: no account rows found in "${args.file}"`);
    return 1;
  }

  const duplicateNames = findDuplicateNames(listingRows);
  if (duplicateNames.length > 0) {
    console.error(
      `${duplicateNames.length} account name(s) appear more than once in the listing ` +
        `itself — never inserting the same account twice, aborting before any database write:`,
    );
    for (const name of duplicateNames) console.error(`  "${name}"`);
    return 1;
  }

  // Validation-only pass: no existing codes yet (we haven't touched the
  // database), so this only tells us whether every QuickBooks type in the
  // file is recognized. The codes it assigns are provisional and are
  // recomputed inside loadChart(), after it knows what codes the company
  // already has.
  const preliminaryPlan = planAccounts(listingRows);
  if (preliminaryPlan.unknownTypes.length > 0) {
    console.error(
      `${preliminaryPlan.unknownTypes.length} account(s) have an unrecognized QuickBooks type ` +
        `— never guessing, aborting before any database write:`,
    );
    for (const offender of preliminaryPlan.unknownTypes) {
      console.error(`  "${offender.name}": unknown type "${offender.qbType}"`);
    }
    return 1;
  }

  const client = new Client({ connectionString: await databaseUrl() });
  await client.connect();
  try {
    await client.query("BEGIN");

    const company = await ensureCompanyAndOwner(client, {
      name: args.company, ownerEmail: args.ownerEmail, ownerName: args.ownerName,
    });
    const chart = await loadChart(client, company.companyId, listingRows);

    if (chart.conflicts.length > 0) {
      console.error(
        `${chart.conflicts.length} account(s) already exist with a DIFFERENT type ` +
          `than this listing maps them to — never silently mutating, aborting:`,
      );
      for (const c of chart.conflicts) {
        console.error(
          `  "${c.name}": existing type "${c.existingType}", ` +
            `listing says "${c.mappedType}" (QuickBooks type "${c.qbType}")`,
        );
      }
      await client.query("ROLLBACK");
      return 1;
    }

    await client.query("COMMIT");

    console.log(`company "${args.company}": ${company.companyCreated ? "created" : "reused"} (${company.companyId})`);
    console.log(`owner "${args.ownerEmail}": ${company.ownerCreated ? "created" : "reused"}, ` +
      `membership ${company.membershipCreated ? "created" : "already present"}`);
    console.log(`accounts: ${chart.accountsCreated} created, ${chart.accountsSkipped} already present ` +
      `(${chart.totalInListing} total in listing)`);
    console.log(`aliases: ${chart.aliases.created} created, ${chart.aliases.skipped} already present, ` +
      `${chart.aliases.ambiguous.length} ambiguous leaves skipped`);
    if (chart.aliases.ambiguous.length > 0) {
      console.log(`  ambiguous leaves (no alias created — shared by >1 account):`);
      for (const leaf of chart.aliases.ambiguous) console.log(`    "${leaf}"`);
    }
    if (chart.aliases.missing.length > 0) {
      console.error(`  WARNING: ${chart.aliases.missing.length} alias target account(s) not found ` +
        `after insert (this should not happen):`);
      for (const name of chart.aliases.missing) console.error(`    "${name}"`);
    }

    return 0;
  } catch (err) {
    await client.query("ROLLBACK").catch(() => {});
    throw err;
  } finally {
    await client.end();
  }
}

const isMain = process.argv[1] &&
  path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isMain) {
  run(process.argv.slice(2))
    .then((code) => { process.exitCode = code; })
    .catch((err) => { console.error(err); process.exitCode = 1; });
}
