// Tiny hash-based view switcher — Task 8 deliberately avoids adding a router
// dependency for five static top-level screens ("Ask before adding any
// dependency", CLAUDE.md §2). Task 9 extends this minimally to carry an id
// param for the two drill-through pages the reports/dashboard screens link
// to: #/account/:id (an account's entry list) and #/entry/:id (one entry's
// detail) — CLAUDE.md §9 "every number drills through". Browser back/forward
// already works for free because these are ordinary hashchange events; the
// drill pages additionally render their own in-app "Back" link (Task 9 item 5).

import { useCallback, useEffect, useState } from "react";

// "setup" (Task 10) isn't one of the five sidebar screens — it's reached
// from the zero-companies first-run placeholder, the company switcher's
// "Add company" item, and Settings' "+ Add another company" — but it's still a
// plain top-level ViewName (no id to carry, unlike the account/entry
// drill-through routes below), so #setup round-trips through
// reload/back-forward the same way the other five do.
//
// The one wrinkle it grew: the "add a company" doors state an intent, and the
// wizard used to discard it and re-derive one from whichever company happened
// to be selected (see setup/setupEntry.ts). That intent now rides in the hash
// as #/setup?new=1 — a { view: "setup"; intent } Route arm, exactly the shape
// #/transactions/account/:id already uses. In the hash specifically, rather
// than in sessionStorage or component state, because it has to survive F5:
// reloading the page you are standing on must not change what you are doing,
// and a one-shot handoff would quietly turn "name your new company" back into
// "resume the other one" on refresh.
//
// "customers" and "invoices" (AR Task 5) are two more static top-level
// screens, same shape as the five above — #customers and #invoices carry no
// id of their own (the id-carrying invoice routes are the separate "invoice"/
// "invoice-print" Route arms below, mirroring "account"/"entry").
//
// "vendors" and "bills" (AP Task 4) are the same shape again, mirroring
// customers/invoices — #vendors and #bills carry no id of their own (the
// id-carrying bill route is the separate "bill" Route arm below, mirroring
// "invoice" — there is deliberately no "bill-print" arm: bills aren't printed,
// per the AP design spec).
//
// "checks" (AP Task 5) is one more static top-level screen — the
// checks-to-print queue and print-run setup (ChecksScreen.tsx) — same shape,
// no id of its own (the print route itself is the separate "checks-print"
// Route arm below, carrying a params bag, same shape as "receive-payment").
//
// "home" (Bank Rec Task 4) is the reconcile-first home screen — the new
// DEFAULT landing view (see parseHash's fallback below) — same static shape
// as the others, no id of its own. It sits FIRST in the sidebar, ahead of
// "dashboard", which stays reachable on its own for anyone who wants the
// classic view (design spec: "Dashboard remains one click").
// "transactions" (Switch From Anywhere Task 2) is the transaction register —
// one more static top-level screen, same shape as the others, no id of its
// own (its account/filter state lives in the screen, not the hash; the
// id-carrying drill-through out of it is the existing "entry" arm below).
// "chart" (Chart Management Task 2) is the Chart of Accounts screen — one
// more static top-level screen, same shape as the others. The one id-carrying
// wrinkle this task adds lives on "transactions" instead: a chart row click
// lands on #/transactions/account/:id — the SAME Transactions screen with
// that account preselected (a { view: "transactions"; accountId } Route arm
// below, so the sidebar still highlights Transactions) — while the bare
// #/transactions keeps working unchanged.
// "items" and "purchase-orders" (Inventory Task 4) are two more static
// top-level screens of the same shape — #items and #purchase-orders carry no
// id of their own. Their id-carrying drill-throughs are the separate "item",
// "purchase-order" and "purchase-order-print" Route arms below, mirroring
// "invoice"/"invoice-print" exactly.
// "builds" and "stock-adjustments" (Inventory Task 5) are two more of the same
// static shape — #builds is the build screen plus its history, and
// #stock-adjustments carries both stock corrections and the one-time opening
// count as tabs (its own screen decides which; neither needs a hash of its
// own). Both are hidden from the sidebar for a company with no stocked items
// (Sidebar.tsx), but the routes themselves stay reachable, exactly as #setup
// is reachable without being a nav item.
export type ViewName =
  | "home" | "dashboard" | "reports" | "record" | "close" | "settings" | "setup"
  | "customers" | "invoices" | "vendors" | "bills" | "checks" | "transactions" | "chart"
  | "items" | "purchase-orders" | "builds" | "stock-adjustments";

const VIEWS: readonly ViewName[] = [
  "home", "dashboard", "reports", "record", "close", "settings", "setup", "customers", "invoices",
  "vendors", "bills", "checks", "transactions", "chart", "items", "purchase-orders",
  "builds", "stock-adjustments",
];

function isViewName(value: string): value is ViewName {
  return (VIEWS as readonly string[]).includes(value);
}

/** The two statement print views (AR Task 4) — #reports/print/pnl and
 * #reports/print/balance-sheet, each carrying its report's own query params
 * (from/to, or asOf) as a plain string map so App.tsx can hand them straight
 * to PrintPnl/PrintBalanceSheet without this router knowing their shapes. */
// "inventory-valuation" (Inventory Task 5) joins them: a stock valuation has
// to print to the same standard as the statements it ties to, so it gets the
// same chrome-free page and the same asOf param shape as the balance sheet.
export type PrintType = "pnl" | "balance-sheet" | "inventory-valuation";

const PRINT_TYPES: readonly PrintType[] = ["pnl", "balance-sheet", "inventory-valuation"];

function isPrintType(value: string): value is PrintType {
  return (PRINT_TYPES as readonly string[]).includes(value);
}

/** The parsed current location: one of the seven static top-level screens, a
 * drill-through page carrying the id it's showing, or a print view carrying
 * its report params. "invoice" (#invoice/:id) and "invoice-print"
 * (#invoice/:id/print) are AR Task 5's own id-carrying routes — same shape
 * as "account"/"entry", just distinguished by the extra /print segment
 * (mirroring how #reports/print/:type is distinguished from #reports).
 * "payment" (#payment/:id, AR Task 6) is one more of that same shape.
 * "receive-payment" (#receive-payment?customerId=&invoiceId=, AR Task 6) is
 * the "I got paid" flow's own top-level route — it carries no single id
 * (unlike account/entry/invoice/payment) but an optional customerId/invoiceId
 * pair so its three entry points (Record's "From a customer invoice", the
 * #invoices "Receive payment" button, an open invoice's own "Receive
 * payment" button) can each hand it a different starting point, same
 * params-bag shape as the "print" route above.
 * "bill" (#bill/:id, AP Task 4) is the bills mirror of "invoice" — same
 * shape, deliberately with no "bill-print" counterpart (bills aren't
 * printed, per the AP design spec).
 * "bill-payment" (#bill-payment/:id, AP Task 5) is the bill-payments mirror
 * of "payment" — same shape. "pay-bills" (#pay-bills?vendorId=&billId=, AP
 * Task 5) is the "I paid a bill" flow's own top-level route, same params-bag
 * shape as "receive-payment" (an optional vendorId/billId pair so its entry
 * points — Record's "A bill I owe", a bill's own "Pay this bill" button —
 * can each hand it a different starting point). "checks-print"
 * (#checks/print-run?accountId=&paymentIds=&format=&offsetX=&offsetY=, AP
 * Task 5) is the check print run's own route, carrying everything the run
 * needs to assign numbers and render — same params-bag shape again.
 * "bank-import" (#bank/import?accountId=, Bank Rec Task 4) is the statement
 * import wizard's own route — an optional accountId (pre-selects the
 * account when reached from a home account card's "Import statement"
 * button; the wizard's own step 1 lets it be changed) is its only param,
 * same params-bag shape as "receive-payment"/"pay-bills". "bank-statement"
 * (#bank/statement/:id, Bank Rec Task 4) is the statement review workbench's
 * route — same {view, id} shape as "account"/"entry"/"invoice"/etc. It's
 * wired up THIS task (so the home screen's "Continue reviewing" and the
 * import wizard's "Review now" have somewhere to navigate) even though the
 * real workbench UI doesn't land until Task 5 — until then it renders a
 * placeholder panel (bank/StatementReview.tsx). */
/** The intents an entry point can state when it sends the user to #setup.
 *  Only "new-company" exists today (#/setup?new=1); a bare #/setup stays the
 *  plain { view: "setup" } arm, meaning "nothing stated". */
export type SetupIntent = "new-company";

export type Route =
  | { view: ViewName }
  | { view: "setup"; intent: SetupIntent }
  | { view: "transactions"; accountId: string }
  | { view: "account"; id: string }
  | { view: "entry"; id: string }
  | { view: "invoice"; id: string }
  | { view: "invoice-print"; id: string }
  | { view: "payment"; id: string }
  | { view: "bill"; id: string }
  | { view: "bill-payment"; id: string }
  | { view: "item"; id: string }
  | { view: "purchase-order"; id: string }
  | { view: "purchase-order-print"; id: string }
  | { view: "print"; printType: PrintType; params: Record<string, string> }
  | { view: "receive-payment"; params: Record<string, string> }
  | { view: "pay-bills"; params: Record<string, string> }
  | { view: "checks-print"; params: Record<string, string> }
  | { view: "bank-import"; params: Record<string, string> }
  | { view: "bank-statement"; id: string };

/** What callers pass to navigate(): a bare ViewName for the static screens,
 * an {view, id} pair for the drill-through pages, or an {view, printType,
 * params} triple for the print views. */
export type NavTarget =
  | ViewName
  | { view: "setup"; intent: SetupIntent }
  | { view: "transactions"; accountId: string }
  | { view: "account"; id: string }
  | { view: "entry"; id: string }
  | { view: "invoice"; id: string }
  | { view: "invoice-print"; id: string }
  | { view: "payment"; id: string }
  | { view: "bill"; id: string }
  | { view: "bill-payment"; id: string }
  | { view: "item"; id: string }
  | { view: "purchase-order"; id: string }
  | { view: "purchase-order-print"; id: string }
  | { view: "print"; printType: PrintType; params: Record<string, string> }
  | { view: "receive-payment"; params: Record<string, string> }
  | { view: "pay-bills"; params: Record<string, string> }
  | { view: "checks-print"; params: Record<string, string> }
  | { view: "bank-import"; params: Record<string, string> }
  | { view: "bank-statement"; id: string };

/** The parser, over a hash STRING rather than over `window` — so the routes
 *  can be tested in this repo's window-less test setup (tests/web has no DOM).
 *  parseHash() below is this plus "read the current location". */
export function parseHashString(hash: string): Route {
  const raw = hash.replace(/^#\/?/, "");
  const [pathPart, queryPart] = raw.split("?");
  const segments = (pathPart ?? "").split("/").filter(Boolean);
  const [head, id] = segments;
  // #/transactions/account/:id — the register with an account preselected
  // (Chart Management Task 2). Must be checked before the generic ViewName
  // fallthrough below, which would otherwise match the bare "transactions"
  // head and drop the account segment.
  if (head === "transactions" && id === "account" && segments[2]) {
    return { view: "transactions", accountId: segments[2] };
  }
  if (head === "account" && id) return { view: "account", id };
  if (head === "entry" && id) return { view: "entry", id };
  if (head === "payment" && id) return { view: "payment", id };
  if (head === "invoice" && id) {
    return segments[2] === "print" ? { view: "invoice-print", id } : { view: "invoice", id };
  }
  if (head === "bill" && id) return { view: "bill", id };
  if (head === "bill-payment" && id) return { view: "bill-payment", id };
  if (head === "item" && id) return { view: "item", id };
  if (head === "purchase-order" && id) {
    return segments[2] === "print"
      ? { view: "purchase-order-print", id }
      : { view: "purchase-order", id };
  }
  if (head === "receive-payment") {
    const params = Object.fromEntries(new URLSearchParams(queryPart ?? ""));
    return { view: "receive-payment", params };
  }
  if (head === "pay-bills") {
    const params = Object.fromEntries(new URLSearchParams(queryPart ?? ""));
    return { view: "pay-bills", params };
  }
  if (head === "checks" && id === "print-run") {
    const params = Object.fromEntries(new URLSearchParams(queryPart ?? ""));
    return { view: "checks-print", params };
  }
  if (head === "reports" && id === "print" && segments[2] && isPrintType(segments[2])) {
    const params = Object.fromEntries(new URLSearchParams(queryPart ?? ""));
    return { view: "print", printType: segments[2], params };
  }
  if (head === "bank" && id === "statement" && segments[2]) {
    return { view: "bank-statement", id: segments[2] };
  }
  if (head === "bank" && id === "import") {
    const params = Object.fromEntries(new URLSearchParams(queryPart ?? ""));
    return { view: "bank-import", params };
  }
  // #/setup?new=1 — "I want to add a company", stated by the door the user
  // came through. Checked before the generic ViewName fallthrough below, which
  // would otherwise match the bare "setup" head and drop the intent.
  if (head === "setup") {
    const params = new URLSearchParams(queryPart ?? "");
    if (params.get("new") === "1") return { view: "setup", intent: "new-company" };
    return { view: "setup" };
  }
  if (head && isViewName(head)) return { view: head };
  // A truly empty/unrecognized hash used to default to "dashboard" (Task 8).
  // Bank Rec Task 4 makes "home" the new default landing view instead — see
  // home/Home.tsx's own header for why the ACTUAL "home vs dashboard"
  // eligibility decision lives there (this parser has no access to company
  // data), not here.
  return { view: "home" };
}

function parseHash(): Route {
  return parseHashString(window.location.hash);
}

export function hashFor(target: NavTarget): string {
  if (typeof target === "string") return `/${target}`;
  if (target.view === "setup") return target.intent === "new-company" ? "/setup?new=1" : "/setup";
  if (target.view === "transactions") return `/transactions/account/${target.accountId}`;
  if (target.view === "print") {
    const qs = new URLSearchParams(target.params).toString();
    return `/reports/print/${target.printType}${qs ? `?${qs}` : ""}`;
  }
  if (target.view === "receive-payment") {
    const qs = new URLSearchParams(target.params).toString();
    return `/receive-payment${qs ? `?${qs}` : ""}`;
  }
  if (target.view === "pay-bills") {
    const qs = new URLSearchParams(target.params).toString();
    return `/pay-bills${qs ? `?${qs}` : ""}`;
  }
  if (target.view === "checks-print") {
    const qs = new URLSearchParams(target.params).toString();
    return `/checks/print-run${qs ? `?${qs}` : ""}`;
  }
  if (target.view === "bank-import") {
    const qs = new URLSearchParams(target.params).toString();
    return `/bank/import${qs ? `?${qs}` : ""}`;
  }
  if (target.view === "bank-statement") return `/bank/statement/${target.id}`;
  if (target.view === "invoice-print") return `/invoice/${target.id}/print`;
  if (target.view === "purchase-order-print") return `/purchase-order/${target.id}/print`;
  return `/${target.view}/${target.id}`;
}

function routeFor(target: NavTarget): Route {
  return typeof target === "string" ? { view: target } : target;
}

/**
 * Rewrites the current hash in place — no new history entry, no hashchange,
 * no re-render. For the narrow case of a route param that has been CONSUMED:
 * #/setup?new=1 means "I am about to add a company", and once that company
 * exists the statement is spent. Leaving it in the URL would make F5 restart
 * the naming step over a company the user just created (and abandon it);
 * pushing a fresh entry instead would leave a Back step that silently re-arms
 * the same intent. Replacing does neither: reload resumes the setup actually
 * in progress, and Back still goes wherever the user came from.
 */
export function replaceHash(target: NavTarget): void {
  window.history.replaceState(null, "", `#${hashFor(target)}`);
}

export function useHashView(): [Route, (target: NavTarget) => void] {
  const [route, setRoute] = useState<Route>(parseHash);

  useEffect(() => {
    const onHashChange = () => setRoute(parseHash());
    window.addEventListener("hashchange", onHashChange);
    return () => window.removeEventListener("hashchange", onHashChange);
  }, []);

  const navigate = useCallback((target: NavTarget) => {
    window.location.hash = hashFor(target);
    setRoute(routeFor(target));
  }, []);

  return [route, navigate];
}
