import type { UniDeskConfig } from "../config";
import type { RenderedCliResult } from "../output";
import { renderTable } from "./render";
import { boolField, compactCapture, parseJsonOutput, runRemoteCodexPoolScript } from "./remote";
import { creditsScript } from "./remote-scripts";
import { codexPoolRuntimeTarget, defaultCodexPoolRuntimeTargetId } from "./runtime-target";
import { readCodexPoolConfig } from "./config";
import type { CodexPoolCreditRankTier, CodexPoolCreditsConfig } from "./types";

interface CreditsOptions {
  activeSince: string | null;
  activeStart: string | null;
  activeEnd: string | null;
  activeStartUtc: string | null;
  activeEndUtc: string | null;
  timezone: string;
  excludedUsers: string[];
  users: string[];
  amountUsd: number | null;
  rankTiers: CodexPoolCreditRankTier[];
  rankTiersSource: "yaml-default" | "cli" | null;
  allocations: Record<string, number>;
  reason: string;
  confirm: boolean;
  targetId: string;
  json: boolean;
}

export async function codexPoolCredits(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
  if (args.includes("--help")) return renderCreditsHelp();
  const pool = readCodexPoolConfig();
  const options = parseCreditsOptions(args, pool.credits);
  const target = codexPoolRuntimeTarget(options.targetId);
  const remote = await runRemoteCodexPoolScript(config, "credits", creditsScript(options, pool, target), target);
  const report = parseJsonOutput(remote.stdout);
  const ok = remote.exitCode === 0 && boolField(report, "ok", false);
  const response = {
    ok,
    action: "platform-infra-sub2api-codex-pool-credits",
    target: { id: target.id, route: target.route, runtimeMode: target.runtimeMode, endpoint: target.serviceDns },
    request: {
      selection: Object.keys(options.allocations).length > 0
        ? "exact-allocations"
        : options.users.length > 0
          ? "exact-users"
          : options.activeSince === null ? "active-window" : "active-users",
      activeSince: options.activeSince,
      activeStart: options.activeStart,
      activeEnd: options.activeEnd,
      activeStartUtc: options.activeStartUtc,
      activeEndUtc: options.activeEndUtc,
      timezone: options.timezone,
      excludedUsers: options.excludedUsers,
      users: options.users,
      amountUsd: options.amountUsd,
      rankTiers: options.rankTiers,
      rankTiersSource: options.rankTiersSource,
      allocations: options.allocations,
      reason: options.reason,
      confirm: options.confirm,
    },
    report,
    remote: compactCapture(remote, { full: !ok || report === null }),
    valuesPrinted: false,
  };
  return options.json ? renderCreditsJson(response) : renderCredits(response);
}

function parseCreditsOptions(args: string[], creditsConfig: CodexPoolCreditsConfig): CreditsOptions {
  const { timezone, excludedUsers } = creditsConfig;
  const [action = "grant", ...rest] = args;
  if (action !== "grant") throw new Error("credits usage: grant (--active-since 24h|--active-start <ISO> --active-end <ISO>|--users <selectors>|--allocations-json <object>) [--amount-usd <amount>|--rank-tiers <count:amount,...,*:amount>] --reason <text> [--confirm] [--target <id>] [--json]");
  let activeSince: string | null = null;
  let activeStart: string | null = null;
  let activeEnd: string | null = null;
  let users: string[] = [];
  let amountUsd: number | null = null;
  let rankTiers: CodexPoolCreditRankTier[] = [];
  let rankTiersSource: CreditsOptions["rankTiersSource"] = null;
  let allocations: Record<string, number> = {};
  let reason: string | null = null;
  let confirm = false;
  let targetId = defaultCodexPoolRuntimeTargetId();
  let json = false;
  const readValue = (index: number, option: string): [string, number] => {
    const value = rest[index + 1];
    if (value === undefined || value.startsWith("--")) throw new Error(`${option} requires a value`);
    return [value.trim(), index + 1];
  };
  for (let index = 0; index < rest.length; index += 1) {
    const arg = rest[index]!;
    if (arg === "--confirm") confirm = true;
    else if (arg === "--json") json = true;
    else if (arg === "--active-since") [activeSince, index] = readValue(index, arg);
    else if (arg.startsWith("--active-since=")) activeSince = arg.slice("--active-since=".length).trim();
    else if (arg === "--active-start") [activeStart, index] = readValue(index, arg);
    else if (arg.startsWith("--active-start=")) activeStart = arg.slice("--active-start=".length).trim();
    else if (arg === "--active-end") [activeEnd, index] = readValue(index, arg);
    else if (arg.startsWith("--active-end=")) activeEnd = arg.slice("--active-end=".length).trim();
    else if (arg === "--users") {
      let value: string;
      [value, index] = readValue(index, arg);
      users = parseSelectors(value);
    } else if (arg.startsWith("--users=")) users = parseSelectors(arg.slice("--users=".length));
    else if (arg === "--amount-usd") {
      let value: string;
      [value, index] = readValue(index, arg);
      amountUsd = parseAmount(value);
    } else if (arg.startsWith("--amount-usd=")) amountUsd = parseAmount(arg.slice("--amount-usd=".length));
    else if (arg === "--rank-tiers") {
      let value: string;
      [value, index] = readValue(index, arg);
      rankTiers = parseRankTiers(value);
      rankTiersSource = "cli";
    } else if (arg.startsWith("--rank-tiers=")) {
      rankTiers = parseRankTiers(arg.slice("--rank-tiers=".length));
      rankTiersSource = "cli";
    }
    else if (arg === "--allocations-json") {
      let value: string;
      [value, index] = readValue(index, arg);
      allocations = parseAllocations(value);
    } else if (arg.startsWith("--allocations-json=")) allocations = parseAllocations(arg.slice("--allocations-json=".length));
    else if (arg === "--reason") [reason, index] = readValue(index, arg);
    else if (arg.startsWith("--reason=")) reason = arg.slice("--reason=".length).trim();
    else if (arg === "--target") [targetId, index] = readValue(index, arg);
    else if (arg.startsWith("--target=")) targetId = arg.slice("--target=".length).trim();
    else throw new Error(`unsupported credits option: ${arg}`);
  }
  if ((activeStart === null) !== (activeEnd === null)) throw new Error("--active-start and --active-end must be provided together");
  const selectionCount = Number(activeSince !== null) + Number(activeStart !== null) + Number(users.length > 0) + Number(Object.keys(allocations).length > 0);
  if (selectionCount !== 1) throw new Error("use exactly one of --active-since, --active-start/--active-end, --users, or --allocations-json");
  if (activeSince !== null && !/^[1-9][0-9]*(?:m|h|d)$/u.test(activeSince)) throw new Error("--active-since must use a duration such as 24h");
  if (activeStart !== null && !isIsoDateTime(activeStart)) throw new Error("--active-start must use ISO date-time format");
  if (activeEnd !== null && !isIsoDateTime(activeEnd)) throw new Error("--active-end must use ISO date-time format");
  const activeStartUtc = activeStart === null ? null : isoInTimezoneToUtc(activeStart, timezone);
  const activeEndUtc = activeEnd === null ? null : isoInTimezoneToUtc(activeEnd, timezone);
  if (activeStartUtc !== null && activeEndUtc !== null && activeEndUtc <= activeStartUtc) {
    throw new Error("credits active window end must be after start");
  }
  const activeSelection = activeSince !== null || activeStart !== null;
  if (rankTiers.length > 0 && !activeSelection) throw new Error("--rank-tiers requires an active user window");
  if (rankTiers.length > 0 && amountUsd !== null) throw new Error("use exactly one of --amount-usd or --rank-tiers");
  if (Object.keys(allocations).length > 0 && (amountUsd !== null || rankTiers.length > 0)) {
    throw new Error("--allocations-json already contains per-user amounts");
  }
  if (activeSelection && amountUsd === null && rankTiers.length === 0 && Object.keys(allocations).length === 0) {
    rankTiers = creditsConfig.defaultRankTiers.map((tier) => ({ ...tier }));
    rankTiersSource = "yaml-default";
  } else if (amountUsd === null && rankTiers.length === 0 && Object.keys(allocations).length === 0) {
    throw new Error("credits grant requires --amount-usd, --rank-tiers, or --allocations-json");
  }
  if (confirm && activeSelection) throw new Error("confirmed credits require exact --users or --allocations-json selectors returned by a dry-run");
  if (confirm && users.length === 0 && Object.keys(allocations).length === 0) {
    throw new Error("confirmed credits require exact selectors returned by an active-user dry-run");
  }
  if (reason === null || reason.length === 0 || reason.length > 500 || /[\r\n\0]/u.test(reason)) throw new Error("credits grant requires a single-line --reason up to 500 characters");
  return { activeSince, activeStart, activeEnd, activeStartUtc, activeEndUtc, timezone, excludedUsers, users, amountUsd, rankTiers, rankTiersSource, allocations, reason, confirm, targetId, json };
}

function isIsoDateTime(value: string): boolean {
  return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,6})?)?(?:Z|[+-]\d{2}:\d{2})?$/u.test(value);
}

function isoInTimezoneToUtc(value: string, timezone: string): string {
  if (/(?:Z|[+-]\d{2}:\d{2})$/u.test(value)) {
    const instant = new Date(value);
    if (!Number.isFinite(instant.getTime())) throw new Error("fixed-window date-time is invalid");
    return instant.toISOString();
  }
  const matched = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,6}))?)?$/u.exec(value);
  if (matched === null) throw new Error("fixed-window date-time is invalid");
  const components = matched.slice(1, 7).map((item) => Number(item ?? 0));
  const milliseconds = Number((matched[7] ?? "").padEnd(3, "0").slice(0, 3));
  const localEpoch = Date.UTC(components[0]!, components[1]! - 1, components[2]!, components[3]!, components[4]!, components[5]!, milliseconds);
  let instant = localEpoch;
  for (let attempt = 0; attempt < 3; attempt += 1) {
    const actual = timezoneComponents(instant, timezone);
    const represented = Date.UTC(actual[0], actual[1] - 1, actual[2], actual[3], actual[4], actual[5], milliseconds);
    instant = localEpoch - (represented - instant);
  }
  const actual = timezoneComponents(instant, timezone);
  if (actual.some((item, index) => item !== components[index])) {
    throw new Error(`fixed-window date-time does not exist in timezone ${timezone}: ${value}`);
  }
  return new Date(instant).toISOString();
}

function timezoneComponents(epochMs: number, timezone: string): number[] {
  const parts = new Intl.DateTimeFormat("en-CA", {
    timeZone: timezone,
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
    hourCycle: "h23",
  }).formatToParts(new Date(epochMs));
  const values = new Map(parts.map((part) => [part.type, Number(part.value)]));
  return ["year", "month", "day", "hour", "minute", "second"].map((key) => values.get(key) ?? Number.NaN);
}

function parseSelectors(value: string): string[] {
  const selectors = value.split(",").map((item) => item.trim()).filter(Boolean);
  if (selectors.length === 0 || selectors.length > 200) throw new Error("--users requires 1 to 200 comma-separated exact IDs or emails");
  if (selectors.some((item) => item.length > 256 || /[\r\n\0]/u.test(item))) throw new Error("--users contains an unsupported selector");
  if (new Set(selectors).size !== selectors.length) throw new Error("--users contains duplicate selectors");
  return selectors;
}

function parseAmount(value: string): number {
  const amount = Number(value);
  if (!Number.isFinite(amount) || amount <= 0) throw new Error("--amount-usd must be greater than zero");
  return amount;
}

function parseRankTiers(value: string): CodexPoolCreditRankTier[] {
  const parts = value.split(",").map((item) => item.trim()).filter(Boolean);
  if (parts.length === 0 || parts.length > 20) throw new Error("--rank-tiers requires 1 to 20 comma-separated count:amount tiers");
  const tiers = parts.map((part, index) => {
    const matched = /^(\*|[1-9][0-9]*):(.+)$/u.exec(part);
    if (matched === null) throw new Error("--rank-tiers must use count:amount entries such as 3:100,2:50,*:20");
    const count = matched[1] === "*" ? null : Number(matched[1]);
    if (count === null && index !== parts.length - 1) throw new Error("--rank-tiers wildcard must be the final tier");
    return { count, amountUsd: parseAmount(matched[2]!) };
  });
  if (tiers.at(-1)?.count !== null) throw new Error("--rank-tiers requires a final *:amount tier");
  return tiers;
}

function parseAllocations(value: string): Record<string, number> {
  let parsed: unknown;
  try { parsed = JSON.parse(value); } catch { throw new Error("--allocations-json requires a JSON object"); }
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("--allocations-json requires a JSON object");
  const entries = Object.entries(parsed);
  if (entries.length === 0 || entries.length > 200) throw new Error("--allocations-json requires 1 to 200 exact user selectors");
  const allocations: Record<string, number> = {};
  for (const [selector, amount] of entries) {
    const normalized = selector.trim();
    if (normalized.length === 0 || normalized.length > 256 || /[\r\n\0]/u.test(normalized)) {
      throw new Error("--allocations-json contains an unsupported selector");
    }
    allocations[normalized] = parseAmount(String(amount));
  }
  return allocations;
}

function renderCreditsHelp(): RenderedCliResult {
  return {
    ok: true,
    command: "platform-infra sub2api codex-pool credits --help",
    renderedText: [
      "SUB2API USER CREDITS",
      "Usage:",
      "  bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --active-since 24h --reason <text>",
      "  bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --active-since 24h --amount-usd 20 --reason <text> [--confirm]",
      "  bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --active-start <ISO> --active-end <ISO> --amount-usd 20 --reason <text>",
      "  bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --users <id-or-email,...> --amount-usd 20 --reason <text> [--confirm]",
      "  bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --active-since 24h --rank-tiers '3:100,2:50,*:20' --reason <text>",
      "  bun scripts/cli.ts platform-infra sub2api codex-pool credits grant --allocations-json '<selector-to-amount-object>' --reason <text> [--confirm]",
      "Notes:",
      "  Default mode is dry-run. --active-since selects distinct active non-admin users from native usage.",
      "  Active windows without --amount-usd or --rank-tiers use YAML credits.defaultRankTiers.",
      "  Explicit --amount-usd or --rank-tiers overrides the YAML default.",
      "  Offset-free fixed-window values use config/platform-infra/sub2api-codex-pool.yaml#credits.timezone and the interval is [start,end).",
      "  YAML credits.excludedUsers selectors are excluded from both active-window and exact-user grants.",
      "  --confirm performs additive balance writes and then reads every selected user back.",
      "  Ranked windows are dry-run only. Reuse the emitted --allocations-json plan for one heterogeneous confirmation call.",
    ].join("\n"),
    contentType: "text/plain",
  };
}

function renderCredits(response: Record<string, unknown>): RenderedCliResult {
  const report = record(response.report);
  const summary = record(report.summary);
  const rows = arrayOfRecords(report.items);
  const excludedRows = arrayOfRecords(report.excluded);
  const request = record(response.request);
  const lines = [
    `SUB2API USER CREDITS ok=${response.ok === true} mode=${text(report.mode)} selection=${text(record(report.selection).kind)} selected=${text(summary.selected)} excluded=${text(summary.excluded)} amount-usd=${text(summary.amountUsd)} total-usd=${text(summary.totalAmountUsd)} rank-tiers-source=${text(request.rankTiersSource)}`,
    `WRITE attempted=${text(summary.writeAttempted)} succeeded=${text(summary.writeSucceeded)} failed=${text(summary.writeFailed)} reconciled=${text(summary.reconciled)} mutation=${text(report.mutation)}`,
  ];
  const selection = record(report.selection);
  if (selection.kind === "active-users" || selection.kind === "active-window") {
    lines.push(`WINDOW timezone=${text(selection.timezone)} start=${text(selection.startAt)} end=${text(selection.endAt)} start-utc=${text(selection.startAtUtc)} end-utc=${text(selection.endAtUtc)}`);
  }
  if (typeof report.error === "string") lines.push(`ERROR ${text(report.error)}`);
  if (rows.length > 0) {
    lines.push("", renderTable([
      ["RANK", "EMAIL", "USER_ID", "ROLE", "STATUS", "REQUESTS", "INPUT_TOKENS", "OUTPUT_TOKENS", "TOTAL_TOKENS", "TOKEN_DATA", "BALANCE_BEFORE", "ADD_USD", "BALANCE_AFTER", "WRITE", "RECONCILED"],
      ...rows.map((row) => [
        integer(row.rank), text(row.email), text(row.userId), text(row.role), text(row.status), integer(row.requestCount), integer(row.inputTokens), integer(row.outputTokens),
        integer(row.totalTokens), tokenData(row), usd(row.balanceBefore), usd(row.amountUsd), usd(row.balanceAfter), text(record(row.write).status), text(row.reconciled),
      ]),
    ]));
  }
  if (excludedRows.length > 0) {
    lines.push("", "EXCLUDED", renderTable([
      ["EMAIL", "USER_ID", "REQUESTS", "INPUT_TOKENS", "OUTPUT_TOKENS", "TOTAL_TOKENS", "TOKEN_DATA", "REASON", "MATCHED_SELECTOR"],
      ...excludedRows.map((row) => [
        text(row.email), text(row.userId), integer(row.requestCount), integer(row.inputTokens), integer(row.outputTokens), integer(row.totalTokens),
        tokenData(row), text(row.reason), text(row.matchedSelector),
      ]),
    ]));
  }
  if (report.mode === "dry-run") {
    const confirmationUserIds = Array.isArray(report.confirmationUserIds) ? report.confirmationUserIds : [];
    const confirmationAllocations = record(report.confirmationAllocations);
    const selectors = confirmationUserIds.length > 0 ? confirmationUserIds.join(",") : "<none>";
    lines.push("", `CONFIRM SELECTORS --users ${selectors}`);
    if (Object.keys(confirmationAllocations).length > 0) {
      lines.push(`CONFIRM PLAN --allocations-json '${JSON.stringify(confirmationAllocations)}'`);
    }
    lines.push(confirmationUserIds.length > 0
      ? "NEXT: use these exact selectors with --confirm only after explicit user authorization."
      : "NEXT: no users remain eligible for confirmation.");
  }
  return { ok: response.ok === true, command: "platform-infra sub2api codex-pool credits", renderedText: lines.join("\n"), contentType: "text/plain", projection: response };
}

function renderCreditsJson(response: Record<string, unknown>): RenderedCliResult {
  return { ok: response.ok === true, command: "platform-infra sub2api codex-pool credits --json", renderedText: JSON.stringify(response), contentType: "application/json", projection: response };
}

function record(value: unknown): Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {}; }
function arrayOfRecords(value: unknown): Record<string, unknown>[] { return Array.isArray(value) ? value.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null && !Array.isArray(item)) : []; }
function text(value: unknown): string { return value === null || value === undefined || value === "" ? "-" : String(value).replace(/[\r\n\t]+/gu, " "); }
function usd(value: unknown): string { const numeric = Number(value); return Number.isFinite(numeric) ? `$${numeric.toFixed(8)}` : text(value); }
function integer(value: unknown): string {
  if (value === null || value === undefined || value === "") return "-";
  const numeric = Number(value);
  return Number.isSafeInteger(numeric) ? numeric.toLocaleString("en-US") : text(value);
}
function tokenData(row: Record<string, unknown>): string {
  if (row.tokenCompleteness !== "complete" && row.tokenCompleteness !== "partial") return "unavailable";
  return `${text(row.tokenCompleteness)}:${integer(row.tokenRows)}/${integer(row.requestCount)} rows`;
}
