import type { ColumnDef } from "@tanstack/react-table";

import { labelForGridColumn, type CustomerGridColumn } from "@/app/customer/_lib/customer-grid-columns";
import { CustomerSortableHeader } from "@/components/customer/customer-table-primitives";
import { formatApiDate } from "@/lib/format/dates";
import { EM_DASH as SHARED_EM_DASH, formatAmount } from "@/lib/format/numbers";
import { cn } from "@/lib/utils";

/**
 * Builders for the column definitions every customer asset list repeats.
 *
 * Each `columns.tsx` used to spell these out by hand, so `uid_title`,
 * `placement_date` and `bank` were re-typed in 17 files and the blank-value
 * helper existed as 25 identical private copies under two different names
 * (`displayCell` / `displayBank`).
 *
 * Module-specific columns stay hand-written: amount, quantity and price genuinely
 * differ per module (labels, sign colouring, formatter, empty handling), and
 * forcing them through a builder would cost more in options than it saves.
 */

export const EM_DASH = SHARED_EM_DASH;

/** Blank-safe cell text: renders an em dash instead of an empty cell. */
export function displayCell(value: string | null | undefined): string {
  return value?.trim() ? value : EM_DASH;
}

export type AssetColumnContext = {
  gridColumns: CustomerGridColumn[] | undefined;
};

type AssetColumnConfig<TRow> = {
  /** Grid column id; also the key used to look up the tenant's custom label. */
  id: string;
  accessorKey: string;
  /** Fallback header label when the tenant has not renamed the column. */
  label: string;
  read: (row: TRow) => string;
  className?: string;
  /**
   * Render `EM_DASH` for blank values.
   *
   * Deliberately opt-in: modules currently disagree about this for the same
   * column (e.g. `currency` dashes in 6 modules and renders blank in 7), so the
   * default must not silently change any existing page.
   */
  blankAsDash?: boolean;
};

function headerFor(gridColumns: CustomerGridColumn[] | undefined, id: string, label: string) {
  return function Header({ column }: { column: Parameters<typeof CustomerSortableHeader>[0]["column"] }) {
    return <CustomerSortableHeader label={labelForGridColumn(gridColumns, id, label)} column={column} />;
  };
}

/** Sortable text column. */
export function assetTextColumn<TRow>(
  { gridColumns }: AssetColumnContext,
  {
    id,
    accessorKey,
    label,
    read,
    className = "text-sm",
    blankAsDash = false,
  }: AssetColumnConfig<TRow>,
): ColumnDef<TRow> {
  return {
    id,
    accessorKey,
    header: headerFor(gridColumns, id, label),
    cell: ({ row }) => {
      const value = read(row.original);
      return <span className={className}>{blankAsDash ? displayCell(value) : value}</span>;
    },
  } as ColumnDef<TRow>;
}

/** Sortable date column rendered as `dd MMM yyyy`, falling back to the raw API value. */
export function assetDateColumn<TRow>(
  { gridColumns }: AssetColumnContext,
  {
    id,
    accessorKey,
    label,
    read,
    className = "whitespace-nowrap text-sm",
    blankAsDash = false,
  }: AssetColumnConfig<TRow>,
): ColumnDef<TRow> {
  return {
    id,
    accessorKey,
    header: headerFor(gridColumns, id, label),
    cell: ({ row }) => {
      const raw = read(row.original);
      const formatted = formatApiDate(raw, "dd MMM yyyy", raw);
      return <span className={className}>{blankAsDash ? displayCell(formatted) : formatted}</span>;
    },
  } as ColumnDef<TRow>;
}

/** Row shape the standard lead columns read from. */
export type StandardAssetLeadRow = {
  refId: string;
  placementDate: string;
  bank: string;
};

/**
 * The `Ref. ID` / `Placement Date` / `Bank` triple that opens most asset lists.
 *
 * Modules that interpose another column (fixed-deposit, swaps, cash-withdrawal)
 * or reorder the pair (fx-accumulator) should compose `assetTextColumn` and
 * `assetDateColumn` directly instead.
 */
export function standardAssetLeadColumns<TRow extends StandardAssetLeadRow>(
  ctx: AssetColumnContext,
): ColumnDef<TRow>[] {
  return [
    assetTextColumn<TRow>(ctx, {
      id: "uid_title",
      accessorKey: "refId",
      label: "Ref. ID",
      read: (row) => row.refId,
      className: "whitespace-nowrap text-sm",
    }),
    assetDateColumn<TRow>(ctx, {
      id: "placement_date",
      accessorKey: "placementDate",
      label: "Placement Date",
      read: (row) => row.placementDate,
    }),
    assetTextColumn<TRow>(ctx, {
      id: "bank",
      accessorKey: "bank",
      label: "Bank",
      read: (row) => row.bank,
      blankAsDash: true,
    }),
  ];
}

/** Config for the right-aligned numeric columns. */
type AssetNumericColumnConfig<TRow> = {
  id: string;
  accessorKey: string;
  label: string;
  read: (row: TRow) => number;
  /** Defaults to `formatAmount` (grouped, two decimals, leading minus). */
  format?: (value: number) => string;
  /**
   * Pre-formatted value from the backend, preferred over `format` when present.
   * Several modules render `priceDisplay` / `quantityDisplay` when the API sends
   * one, because the Yii grid rounded some columns server-side.
   */
  readDisplay?: (row: TRow) => string | null | undefined;
  className?: string;
};

/**
 * Right-aligned numeric column with tabular figures.
 *
 * Price and quantity columns across the asset modules are this shape; only the
 * formatter and the optional server-formatted override differ.
 */
export function assetNumericColumn<TRow>(
  { gridColumns }: AssetColumnContext,
  {
    id,
    accessorKey,
    label,
    read,
    format = formatAmount,
    readDisplay,
    className = "block text-right text-sm tabular-nums",
  }: AssetNumericColumnConfig<TRow>,
): ColumnDef<TRow> {
  return {
    id,
    accessorKey,
    header: headerFor(gridColumns, id, label),
    cell: ({ row }) => (
      <span className={className}>
        {readDisplay?.(row.original) ?? format(read(row.original))}
      </span>
    ),
  } as ColumnDef<TRow>;
}

/**
 * Signed money column: red when negative, green when positive, neutral at zero.
 *
 * This exact `cn(...)` ternary was copied verbatim into thirteen `columns.tsx`
 * files, so a change to how a loss is coloured meant thirteen edits.
 */
export function assetSignedAmountColumn<TRow>(
  { gridColumns }: AssetColumnContext,
  {
    id,
    accessorKey,
    label,
    read,
    format = formatAmount,
    readDisplay,
  }: AssetNumericColumnConfig<TRow>,
): ColumnDef<TRow> {
  return {
    id,
    accessorKey,
    header: headerFor(gridColumns, id, label),
    cell: ({ row }) => {
      const value = read(row.original);
      return (
        <span
          className={cn(
            "block text-right font-medium text-sm tabular-nums",
            value < 0
              ? "text-red-600 dark:text-red-400"
              : value > 0
                ? "text-green-600 dark:text-green-400"
                : "",
          )}
        >
          {readDisplay?.(row.original) ?? format(value)}
        </span>
      );
    },
  } as ColumnDef<TRow>;
}

/** Monospaced ticker/symbol column — narrower and non-wrapping. */
export function assetTickerColumn<TRow>(
  ctx: AssetColumnContext,
  config: Omit<AssetColumnConfig<TRow>, "className">,
): ColumnDef<TRow> {
  return assetTextColumn<TRow>(ctx, {
    ...config,
    className: "whitespace-nowrap font-mono text-xs",
    blankAsDash: true,
  });
}

/* --------------------------------------------------- child (sale) row action */

/**
 * Href for the child Sale / Resale form.
 *
 * Eight modules carried a byte-identical private copy of this under eight names
 * (`buildBondSaleFormHref`, `buildCryptoResaleFormHref`, …). Every asset module
 * addresses the child form the same way; only the button label differs.
 */
export function childFormHref(listPath: string, parentId: string): string {
  const params = new URLSearchParams({ pid: parentId.trim() });
  return `${listPath}/form?${params.toString()}`;
}

/**
 * Whether a row may offer its child Sale / Resale action.
 *
 * A row-specific grant or a URL supplied by the backend always wins. Failing
 * that the page-level grant applies, but only to purchase rows — a sale row
 * cannot itself be sold.
 *
 * `isPurchase` is passed as a boolean rather than read off the row because that
 * flag is the single thing the eight previous copies actually varied on: each
 * read its own `isBondPurchase` / `isCryptoPurchase` / `isStructurePurchase`.
 */
export function canShowChildAction(args: {
  rowFlag: boolean | undefined;
  rowUrl: string | null | undefined;
  pageGrant: boolean | undefined;
  isPurchase: boolean | undefined;
}): boolean {
  if (args.rowFlag || args.rowUrl) return true;
  return Boolean(args.pageGrant && args.isPurchase === true);
}
