// web/src/pages/settings/PlansSettings.tsx
import {
  forwardRef,
  useCallback,
  useEffect,
  useImperativeHandle,
  useMemo,
  useState,
  type FormEvent,
} from "react";
import { Money } from "../../components/ui";
import CurrencyInput from "../../components/CurrencyInput";
import { useDashboard } from "../../hooks/useDashboard";
import { useToast } from "../../components/Toast";
import {
  dateStringToUTCMidnight,
  formatDateInTimezone,
  getBrowserTimezone,
  getTodayInTimezone,
  isoToDateString,
} from "../../utils/timezone";
import { fixedPlansApi } from "../../api/fixedPlans";

type FixedPlan = {
  id: string;
  name: string;
  amountMode?: "fixed" | "estimated";
  estimatedCents?: number | null;
  actualCents?: number | null;
  actualRecordedAt?: string | null;
  actualCycleDueOn?: string | null;
  totalCents: number;
  fundedCents: number;
  priority: number;
  dueOn: string;
  cycleStart: string;
  frequency?: "one-time" | "weekly" | "biweekly" | "monthly";
  autoPayEnabled?: boolean;
  paymentSchedule?: any;
  nextPaymentDate?: string | null;
};

type LocalPlan = FixedPlan & { _isNew?: boolean; _isDeleted?: boolean };

function parseCurrencyToCents(value: string): number {
  const cleaned = value.replace(/,/g, ".").replace(/[^0-9.]/g, "");
  const [whole, ...fractionParts] = cleaned.split(".");
  const fraction = fractionParts.join("");
  const normalized = fraction.length > 0 ? `${whole}.${fraction.slice(0, 2)}` : whole;
  const parsed = Number.parseFloat(normalized || "0");
  return Number.isFinite(parsed) ? Math.round(parsed * 100) : 0;
}

export type PlansSettingsHandle = {
  save: () => Promise<boolean>;
};

interface PlansSettingsProps {
  onDirtyChange?: (dirty: boolean) => void;
}

const PlansSettings = forwardRef<PlansSettingsHandle, PlansSettingsProps>(
  function PlansSettings({ onDirtyChange }, ref) {
  const { data, isLoading, error, refetch, isFetching } = useDashboard();
  const plans = (data?.fixedPlans ?? []) as FixedPlan[];
  const { push } = useToast();

  // Get user timezone from dashboard data
  const userTimezone = data?.user?.timezone || getBrowserTimezone();
  const incomeType = data?.user?.incomeType ?? "regular";
  const incomeFrequency = data?.user?.incomeFrequency;
  const firstIncomeDate = data?.user?.firstIncomeDate ?? null;
  
  // Local editable state (preview mode)
  const [localPlans, setLocalPlans] = useState<LocalPlan[]>([]);
  const [initialized, setInitialized] = useState(false);
  const [isSaving, setIsSaving] = useState(false);
  const [deletePrompt, setDeletePrompt] = useState<LocalPlan | null>(null);
  const [actualInputs, setActualInputs] = useState<Record<string, string>>({});
  const [applyingActualByPlan, setApplyingActualByPlan] = useState<Record<string, boolean>>({});
  const [trueUpMessages, setTrueUpMessages] = useState<Record<string, string>>({});

  useEffect(() => {
    if (!initialized && plans.length > 0) {
      setLocalPlans(plans.map((p) => ({ ...p })));
      setInitialized(true);
    }
  }, [plans, initialized]);

  const resetToServer = useCallback(
    (nextPlans: FixedPlan[] = plans) => {
      setLocalPlans(nextPlans.map((p) => ({ ...p })));
      setInitialized(true);
    },
    [plans]
  );

  const activePlans = useMemo(
    () => localPlans.filter((p) => !p._isDeleted),
    [localPlans]
  );

  const hasChanges = useMemo(() => {
    if (localPlans.length === 0) return false;
    if (localPlans.some((p) => p._isNew || p._isDeleted)) return true;

    for (const local of localPlans) {
      if (local._isNew || local._isDeleted) continue;
      const server = plans.find((p) => p.id === local.id);
      if (!server) return true;

      const scheduleEqual =
        JSON.stringify(local.paymentSchedule ?? null) ===
        JSON.stringify(server.paymentSchedule ?? null);

      if (
        local.name !== server.name ||
        local.totalCents !== server.totalCents ||
        (local.amountMode ?? "fixed") !== (server.amountMode ?? "fixed") ||
        (local.estimatedCents ?? null) !== (server.estimatedCents ?? null) ||
        local.priority !== server.priority ||
        local.dueOn !== server.dueOn ||
        (local.autoPayEnabled ?? false) !== (server.autoPayEnabled ?? false) ||
        (local.frequency ?? null) !== (server.frequency ?? null) ||
        !scheduleEqual ||
        (local.nextPaymentDate ?? null) !== (server.nextPaymentDate ?? null)
      ) {
        return true;
      }
    }

    for (const server of plans) {
      if (!localPlans.find((p) => p.id === server.id)) return true;
    }

    return false;
  }, [localPlans, plans]);

  useEffect(() => {
    onDirtyChange?.(hasChanges);
  }, [hasChanges, onDirtyChange]);

  // Form state for adding new plan
  const [name, setName] = useState("");
  const [totalInput, setTotalInput] = useState("");
  const [priority, setPriority] = useState("");
  const [due, setDue] = useState(getTodayInTimezone(userTimezone));
  const [frequency, setFrequency] = useState<"" | FixedPlan["frequency"]>("monthly");
  const [autoPayEnabled, setAutoPayEnabled] = useState(false);
  const [amountMode, setAmountMode] = useState<"fixed" | "estimated">("fixed");

  const totalCents = Math.max(0, parseCurrencyToCents(totalInput));
  const parsedPriority = Math.max(0, Math.floor(Number(priority) || 0));
  const addDisabled = !name.trim() || totalCents <= 0 || !due || isSaving;

  function mapScheduleFrequency(
    nextFrequency?: FixedPlan["frequency"]
  ): "daily" | "weekly" | "biweekly" | "monthly" | "custom" {
    if (nextFrequency === "weekly") return "weekly";
    if (nextFrequency === "biweekly") return "biweekly";
    return "monthly";
  }

  function buildDefaultSchedule(nextFrequency?: FixedPlan["frequency"]) {
    return {
      frequency: mapScheduleFrequency(nextFrequency),
      minFundingPercent: 100,
    };
  }

  const onAdd = (e: FormEvent) => {
    e.preventDefault();
    if (!name.trim()) return;
    const dueOnISO = dateStringToUTCMidnight(due, userTimezone);
    const schedule = autoPayEnabled
      ? buildDefaultSchedule(frequency || undefined)
      : null;
    const nextPaymentDate =
      autoPayEnabled && schedule
        ? calculateNextPaymentDate(dueOnISO, schedule, userTimezone)
        : null;
    const tempId = `temp_${Date.now()}`;
    const newPlan: LocalPlan = {
      id: tempId,
      name: name.trim(),
      amountMode,
      estimatedCents: amountMode === "estimated" ? totalCents : null,
      totalCents,
      fundedCents: 0,
      priority: parsedPriority || localPlans.length + 1,
      dueOn: dueOnISO,
      cycleStart: dueOnISO,
      frequency: frequency || undefined,
      autoPayEnabled,
      paymentSchedule: schedule,
      nextPaymentDate,
      _isNew: true,
    };
    setLocalPlans((prev) => [...prev, newPlan]);
    setName("");
    setTotalInput("");
    setPriority("");
    setDue(getTodayInTimezone(userTimezone));
    setFrequency("monthly");
    setAutoPayEnabled(false);
    setAmountMode("fixed");
  };

  function toUserMidnight(iso: string, timezone: string) {
    const dateStr = isoToDateString(iso, timezone);
    return new Date(dateStringToUTCMidnight(dateStr, timezone));
  }

  function countPayPeriodsBetween(
    startIso: string,
    endIso: string,
    firstIncomeIso: string,
    frequency: NonNullable<typeof incomeFrequency>,
    timezone: string
  ) {
    let count = 0;
    let nextPayDate = toUserMidnight(firstIncomeIso, timezone);
    const normalizedStart = toUserMidnight(startIso, timezone);
    const normalizedEnd = toUserMidnight(endIso, timezone);

    const targetDay = Number(isoToDateString(firstIncomeIso, timezone).split("-")[2] || "1");
    const advanceByPeriod = () => {
      if (frequency === "monthly") {
        const year = nextPayDate.getUTCFullYear();
        const month = nextPayDate.getUTCMonth() + 1;
        const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
        nextPayDate = new Date(Date.UTC(year, month, Math.min(targetDay, daysInMonth)));
      } else {
        const days = frequency === "biweekly" ? 14 : 7;
        nextPayDate = new Date(nextPayDate.getTime() + days * 86_400_000);
      }
    };

    while (nextPayDate < normalizedStart) {
      advanceByPeriod();
    }
    while (nextPayDate < normalizedEnd) {
      count++;
      advanceByPeriod();
    }
    return Math.max(1, count);
  }

  function getFundingAhead(plan: FixedPlan) {
    if (
      incomeType !== "regular" ||
      !incomeFrequency ||
      !firstIncomeDate ||
      !plan.cycleStart ||
      !plan.dueOn
    ) {
      return null;
    }

    const now = new Date().toISOString();
    let cycleStart = plan.cycleStart;
    const dueOn = plan.dueOn;

    let cycleStartDate: Date;
    let dueDate: Date;
    let nowDate: Date;
    try {
      cycleStartDate = toUserMidnight(cycleStart, userTimezone);
      dueDate = toUserMidnight(dueOn, userTimezone);
      nowDate = toUserMidnight(now, userTimezone);
    } catch {
      return null;
    }
    if (cycleStartDate >= dueDate || cycleStartDate > nowDate) {
      cycleStart = now;
    }

    const totalPeriods = countPayPeriodsBetween(
      cycleStart,
      dueOn,
      firstIncomeDate,
      incomeFrequency,
      userTimezone
    );
    const elapsedPeriods = countPayPeriodsBetween(
      cycleStart,
      now,
      firstIncomeDate,
      incomeFrequency,
      userTimezone
    );
    const targetFunded = Math.min(
      plan.totalCents,
      Math.ceil((plan.totalCents * elapsedPeriods) / totalPeriods)
    );
    const aheadBy = Math.max(0, plan.fundedCents - targetFunded);
    return aheadBy > 0 ? aheadBy : null;
  }

  function calculateNextPaymentDate(
    dueOnISO: string,
    schedule: any,
    timezone: string
  ): string | null {
    if (!schedule || !schedule.frequency) return null;
    const dateStr = isoToDateString(dueOnISO, timezone);
    const [year, month, day] = dateStr.split("-").map(Number);
    const base = new Date(Date.UTC(year, month - 1, day));
    const toDateString = (d: Date) =>
      `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(
        d.getUTCDate()
      ).padStart(2, "0")}`;

    switch (schedule.frequency) {
      case "daily":
        base.setUTCDate(base.getUTCDate() + 1);
        break;
      case "weekly": {
        const targetDay = schedule.dayOfWeek ?? 0;
        const currentDay = base.getUTCDay();
        const daysUntilTarget = (targetDay - currentDay + 7) % 7;
        base.setUTCDate(base.getUTCDate() + (daysUntilTarget || 7));
        break;
      }
      case "biweekly":
        base.setUTCDate(base.getUTCDate() + 14);
        break;
      case "monthly": {
        const targetDay = schedule.dayOfMonth;
        const nextMonth = base.getUTCMonth() + 1;
        const nextYear = base.getUTCFullYear() + Math.floor(nextMonth / 12);
        const normalizedMonth = nextMonth % 12;
        const daysInMonth = new Date(Date.UTC(nextYear, normalizedMonth + 1, 0)).getUTCDate();
        base.setUTCFullYear(nextYear, normalizedMonth, targetDay ? Math.min(targetDay, daysInMonth) : base.getUTCDate());
        break;
      }
      case "custom":
        base.setUTCDate(base.getUTCDate() + Math.max(1, Number(schedule.everyNDays || 0)));
        break;
      default:
        return null;
    }

    return dateStringToUTCMidnight(toDateString(base), timezone);
  }

  const onEdit = (id: string, patch: Partial<FixedPlan>) => {
    setLocalPlans((prev) =>
      prev.map((p) => {
        if (p.id !== id) return p;
        const next: LocalPlan = { ...p, ...patch };
        if (patch.frequency !== undefined && next.autoPayEnabled) {
          const schedule = next.paymentSchedule ?? buildDefaultSchedule(patch.frequency);
          next.paymentSchedule = {
            ...schedule,
            frequency: mapScheduleFrequency(patch.frequency),
          };
        }
        if (patch.totalCents !== undefined) {
          next.totalCents = Math.max(0, Math.round(patch.totalCents));
        }
        if (patch.priority !== undefined) {
          next.priority = Math.max(0, Math.floor(patch.priority));
        }
        if (patch.amountMode !== undefined) {
          const mode = patch.amountMode;
          next.amountMode = mode;
          if (mode === "fixed") {
            next.estimatedCents = null;
          } else {
            next.estimatedCents = next.estimatedCents ?? next.totalCents;
          }
        }
        if (patch.estimatedCents !== undefined) {
          const nextEstimate = Math.max(0, Math.round(patch.estimatedCents ?? 0));
          next.estimatedCents = nextEstimate;
          const hasActualForCycle =
            !!next.actualCycleDueOn && next.actualCycleDueOn === next.dueOn;
          if ((next.amountMode ?? "fixed") === "estimated" && !hasActualForCycle) {
            next.totalCents = nextEstimate;
          }
        }
        if (patch.autoPayEnabled !== undefined && !patch.autoPayEnabled) {
          next.nextPaymentDate = null;
        }
        if (next.autoPayEnabled && next.paymentSchedule) {
          const dueOnISO = patch.dueOn ?? next.dueOn;
          next.nextPaymentDate = calculateNextPaymentDate(
            dueOnISO,
            next.paymentSchedule,
            userTimezone
          );
        }
        return next;
      })
    );
  };

  const onDelete = (id: string) => {
    setLocalPlans((prev) =>
      prev
        .map((p) => {
          if (p.id !== id) return p;
          if (p._isNew) return { ...p, _isDeleted: true };
          return { ...p, _isDeleted: true };
        })
        .filter((p) => !(p._isNew && p._isDeleted))
    );
  };

  const onCancel = () => {
    resetToServer();
    push("ok", "Changes discarded");
  };

  const onSave = useCallback(async (): Promise<boolean> => {
    const normalizedPriorityOrder = activePlans
      .slice()
      .sort((a, b) => a.priority - b.priority || a.name.localeCompare(b.name))
      .map((plan, index) => ({ id: plan.id, priority: index + 1 }));
    const priorityById = new Map(
      normalizedPriorityOrder.map((item) => [item.id, item.priority])
    );
    const normalizedPlans = localPlans.map((plan) =>
      plan._isDeleted ? plan : { ...plan, priority: priorityById.get(plan.id) ?? plan.priority }
    );

    for (const plan of localPlans) {
      if (plan._isDeleted) continue;
      if (plan.totalCents < (plan.fundedCents ?? 0)) {
        push(
          "err",
          `Total for ${plan.name} cannot be less than funded amount.`
        );
        return false;
      }
    }

    setIsSaving(true);
    try {
      const toDelete = normalizedPlans.filter((p) => p._isDeleted && !p._isNew);
      for (const plan of toDelete) {
        await fixedPlansApi.delete(plan.id);
      }

      const toCreate = normalizedPlans.filter((p) => p._isNew && !p._isDeleted);
      for (const plan of toCreate) {
        const created = await fixedPlansApi.create({
          name: plan.name,
          totalCents: plan.totalCents,
          fundedCents: plan.fundedCents ?? 0,
          amountMode: plan.amountMode ?? "fixed",
          estimatedCents:
            (plan.amountMode ?? "fixed") === "estimated"
              ? (plan.estimatedCents ?? plan.totalCents)
              : null,
          priority: plan.priority,
          dueOn: plan.dueOn,
          frequency: plan.frequency,
          autoPayEnabled: plan.autoPayEnabled ?? false,
          paymentSchedule: plan.paymentSchedule ?? undefined,
          nextPaymentDate: plan.nextPaymentDate ?? undefined,
        });
        if (plan.autoPayEnabled) {
          try {
            const res = await fixedPlansApi.fundFromAvailable(created.id);
            if (res.funded) {
              const dollars = (res.fundedAmountCents / 100).toFixed(2);
              push("ok", `Funded $${dollars} toward ${plan.name}.`);
            } else {
              push("err", `Not enough budget to fund ${plan.name}.`);
            }
          } catch (err: any) {
            push(
              "err",
              err?.message ?? `Funding ${plan.name} failed`
            );
          }
        }
      }

      const toUpdate = normalizedPlans.filter((p) => !p._isNew && !p._isDeleted);
      for (const local of toUpdate) {
        const server = plans.find((p) => p.id === local.id);
        if (!server) continue;

        const patch: Partial<FixedPlan> = {};
        if (local.name !== server.name) patch.name = local.name;
        if (local.totalCents !== server.totalCents)
          patch.totalCents = local.totalCents;
        if (local.priority !== server.priority)
          patch.priority = local.priority;
        if (local.dueOn !== server.dueOn) patch.dueOn = local.dueOn;
        if ((local.autoPayEnabled ?? false) !== (server.autoPayEnabled ?? false))
          patch.autoPayEnabled = local.autoPayEnabled;
        if (
          JSON.stringify(local.paymentSchedule ?? null) !==
          JSON.stringify(server.paymentSchedule ?? null)
        )
          patch.paymentSchedule = local.paymentSchedule;
        if ((local.frequency ?? null) !== (server.frequency ?? null))
          patch.frequency = local.frequency;
        if ((local.nextPaymentDate ?? null) !== (server.nextPaymentDate ?? null))
          patch.nextPaymentDate = local.nextPaymentDate ?? null;
        if ((local.amountMode ?? "fixed") !== (server.amountMode ?? "fixed"))
          patch.amountMode = local.amountMode ?? "fixed";
        if ((local.estimatedCents ?? null) !== (server.estimatedCents ?? null))
          patch.estimatedCents = local.estimatedCents ?? 0;

        if (Object.keys(patch).length > 0) {
          await fixedPlansApi.update(local.id, patch);
        }

        const paymentPlanEnabled =
          !!local.autoPayEnabled && local.paymentSchedule !== null && local.paymentSchedule !== undefined;
        const amountChanged = local.totalCents !== server.totalCents;
        const dueChanged = local.dueOn !== server.dueOn;

        if (paymentPlanEnabled && (amountChanged || dueChanged)) {
          try {
            const res = await fixedPlansApi.catchUpFunding(local.id);
            if (res.funded) {
              const dollars = (res.fundedAmountCents / 100).toFixed(2);
              push("ok", `Funded $${dollars} toward ${local.name}.`);
            } else if (res.message === "Insufficient available budget") {
              push("err", `Not enough budget to fund ${local.name}.`);
            }
          } catch (err: any) {
            push("err", err?.message ?? `Funding ${local.name} failed`);
          }
        }
      }

      push("ok", "Fixed expenses saved successfully");
      const refreshed = await refetch();
      const nextPlans = (refreshed.data?.fixedPlans ?? plans) as FixedPlan[];
      resetToServer(nextPlans);
      return true;
    } catch (err: any) {
      push("err", err?.message ?? "Save failed");
      return false;
    } finally {
      setIsSaving(false);
    }
  }, [localPlans, plans, refetch, resetToServer, push]);

  const onApplyActual = useCallback(
    async (plan: LocalPlan) => {
      if (plan._isNew) {
        push("err", "Save this plan before applying an actual amount.");
        return;
      }
      const raw = actualInputs[plan.id] ?? "";
      const actualCents = Math.max(0, parseCurrencyToCents(raw));
      setApplyingActualByPlan((prev) => ({ ...prev, [plan.id]: true }));
      setTrueUpMessages((prev) => ({ ...prev, [plan.id]: "" }));
      try {
        const res = await fixedPlansApi.trueUpActual(plan.id, { actualCents });
        setLocalPlans((prev) =>
          prev.map((p) =>
            p.id !== plan.id
              ? p
              : {
                  ...p,
                  totalCents: res.totalCents,
                  fundedCents: res.fundedCents,
                  actualCents: res.actualCents,
                  actualCycleDueOn: p.dueOn,
                  actualRecordedAt: new Date().toISOString(),
                }
          )
        );
        const summary =
          res.deltaCents > 0
            ? `Actual is higher. Pulled $${(res.autoPulledCents / 100).toFixed(2)} from available budget. Remaining shortfall $${(res.remainingShortfallCents / 100).toFixed(2)}.`
            : res.deltaCents < 0
            ? `Actual is lower. Returned $${(res.refundedCents / 100).toFixed(2)} to available budget.`
            : "Actual matches estimate. No adjustment needed.";
        setTrueUpMessages((prev) => ({ ...prev, [plan.id]: summary }));
        push("ok", "Actual amount applied.");
      } catch (err: any) {
        push("err", err?.message ?? "Unable to apply actual amount.");
      } finally {
        setApplyingActualByPlan((prev) => ({ ...prev, [plan.id]: false }));
      }
    },
    [actualInputs, push]
  );

  useImperativeHandle(ref, () => ({ save: onSave }), [onSave]);

  if (isLoading)
    return (
      <div className="muted">Loading fixed expenses...</div>
    );

  if (error) {
    return (
      <div>
        <p className="mb-3">Couldn't load fixed expenses.</p>
        <button
          className="btn"
          onClick={() => refetch()}
          disabled={isFetching}
        >
          {isFetching ? "Retrying…" : "Retry"}
        </button>
      </div>
    );
  }

  return (
    <div className="space-y-6">
      <header className="space-y-1">
        <div className="flex items-center justify-between flex-wrap gap-2">
          <h2 className="text-lg font-semibold">Fixed Expenses</h2>
          {hasChanges && (
            <span className="text-xs text-amber-400">Unsaved changes</span>
          )}
        </div>
        <p className="text-sm muted">
          Bills and recurring expenses that get funded over time until due.
        </p>
      </header>

      {/* Add form */}
      <form onSubmit={onAdd} className="settings-add-form">
        <select
          className="input"
          value={amountMode}
          onChange={(e) => setAmountMode((e.target.value as "fixed" | "estimated") || "fixed")}
        >
          <option value="fixed">Fixed amount</option>
          <option value="estimated">Estimated bill</option>
        </select>
        <input
          className="input"
          placeholder="Name"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
        <CurrencyInput
          className="input"
          placeholder={amountMode === "estimated" ? "Estimated amount" : "Total amount"}
          value={totalInput}
          onValue={setTotalInput}
        />
        {amountMode === "estimated" && (
          <div className="settings-add-form-tip">
            Tip: For variable bills (utilities, usage-based charges), set a slightly higher estimate to avoid due-date shortfalls.
          </div>
        )}
        <input
          className="input"
          placeholder="Due date"
          type="date"
          value={due}
          onChange={(e) => setDue(e.target.value)}
        />
        <select
          className="input"
          value={frequency || ""}
          onChange={(e) =>
            setFrequency((e.target.value || "") as "" | FixedPlan["frequency"])
          }
        >
          <option value="">Frequency</option>
          <option value="one-time">One-time</option>
          <option value="weekly">Weekly</option>
          <option value="biweekly">Biweekly</option>
          <option value="monthly">Monthly</option>
        </select>
        <label className="settings-checkbox-label">
          <input
            type="checkbox"
            checked={autoPayEnabled}
            onChange={(e) => setAutoPayEnabled(e.target.checked)}
          />
          <span>Auto-fund</span>
        </label>
        <button className="btn" disabled={addDisabled}>
          Add
        </button>
      </form>

      {activePlans.length === 0 ? (
        <div className="muted text-sm">No fixed expenses yet.</div>
      ) : (
        <div className="settings-plans-list">
          {activePlans.map((plan) => {
            const aheadCents = getFundingAhead(plan);
            const fundedCents = plan.fundedCents ?? 0;
            const totalCents = plan.totalCents || 1; // Avoid division by zero
            const progressPercent = Math.min(100, (fundedCents / totalCents) * 100);
            return (
            <div key={plan.id} className={`settings-plan-card ${plan.autoPayEnabled ? 'auto-fund' : ''}`}>
              {/* Header row */}
              <div className="settings-plan-header">
                <div className="settings-plan-title">
                  <InlineEditText
                    value={plan.name}
                    onChange={(v) => onEdit(plan.id, { name: v })}
                  />
                </div>
                <div className="settings-plan-amount">
                  <InlineEditMoney
                    cents={plan.totalCents}
                    onChange={(cents) => onEdit(plan.id, { totalCents: cents })}
                  />
                </div>
              </div>

              {/* Details row */}
              <div className="settings-plan-details">
                <div className="settings-plan-detail">
                  <span className="label">Bill Type</span>
                  <select
                    className="input w-36 text-sm"
                    value={plan.amountMode ?? "fixed"}
                    onChange={(e) =>
                      onEdit(plan.id, {
                        amountMode: (e.target.value as "fixed" | "estimated") ?? "fixed",
                      })
                    }
                  >
                    <option value="fixed">Fixed</option>
                    <option value="estimated">Estimated</option>
                  </select>
                </div>
                <div className="settings-plan-detail">
                  <span className="label">Due</span>
                  <InlineEditDate
                    value={plan.dueOn}
                    timezone={userTimezone}
                    onChange={(iso) => onEdit(plan.id, { dueOn: iso })}
                  />
                </div>
                <div className="settings-plan-detail">
                  <span className="label">Freq</span>
                  <InlineEditSelect
                    value={plan.frequency ?? ""}
                    onChange={(v) =>
                      onEdit(plan.id, {
                        frequency: (v || undefined) as FixedPlan["frequency"],
                      })
                    }
                  />
                </div>
                {(plan.amountMode ?? "fixed") === "estimated" && (
                  <div className="settings-plan-detail">
                    <span className="label">Estimate</span>
                    <InlineEditMoney
                      cents={plan.estimatedCents ?? plan.totalCents}
                      onChange={(cents) => onEdit(plan.id, { estimatedCents: cents })}
                    />
                  </div>
                )}
                {aheadCents !== null && (
                  <div className="settings-plan-badge ahead">
                    +{new Intl.NumberFormat("en-US", {
                      style: "currency",
                      currency: "USD",
                      maximumFractionDigits: 0,
                    }).format((aheadCents ?? 0) / 100)} ahead
                  </div>
                )}
              </div>

              {/* Progress bar */}
              <div className="settings-plan-progress">
                <div className="settings-plan-progress-bar">
                  <div 
                    className="settings-plan-progress-fill"
                    style={{ width: `${progressPercent}%` }}
                  />
                </div>
                <div className="settings-plan-progress-text">
                  <Money cents={plan.fundedCents} /> / <Money cents={plan.totalCents} />
                </div>
              </div>

              {(plan.amountMode ?? "fixed") === "estimated" && (
                <div className="settings-plan-status planned">
                  <div className="flex flex-wrap items-end gap-2">
                    <label className="stack gap-1">
                      <span className="label">Actual this cycle</span>
                      <CurrencyInput
                        className="input w-36"
                        value={actualInputs[plan.id] ?? ""}
                        onValue={(nextValue) =>
                          setActualInputs((prev) => ({ ...prev, [plan.id]: nextValue }))
                        }
                        placeholder={((plan.totalCents ?? 0) / 100).toFixed(2)}
                      />
                    </label>
                    <button
                      className="btn btn-sm"
                      onClick={() => void onApplyActual(plan)}
                      disabled={isSaving || !!applyingActualByPlan[plan.id]}
                      type="button"
                    >
                      {applyingActualByPlan[plan.id] ? "Applying..." : "Apply actual"}
                    </button>
                  </div>
                  <div className="text-xs muted mt-2">
                    Keep this estimate slightly high for variable bills, then true-up with the actual amount when posted.
                  </div>
                  {trueUpMessages[plan.id] ? (
                    <div className="text-xs mt-2">{trueUpMessages[plan.id]}</div>
                  ) : null}
                </div>
              )}

              {/* Actions row */}
              <div className="settings-plan-actions">
                <label className="settings-checkbox-label">
                  <input
                    type="checkbox"
                    checked={!!plan.autoPayEnabled}
                    onChange={(e) =>
                      onEdit(plan.id, {
                        autoPayEnabled: e.target.checked,
                        paymentSchedule: e.target.checked
                          ? plan.paymentSchedule ?? buildDefaultSchedule(plan.frequency)
                          : plan.paymentSchedule,
                      })
                    }
                  />
                  <span>
                    {incomeType === "regular" ? "Auto-fund" : "Payment plan"}
                  </span>
                </label>
                <button
                  className="btn btn-sm btn-danger"
                  onClick={() => setDeletePrompt(plan)}
                  disabled={isSaving}
                >
                  Delete
                </button>
              </div>

              {plan.autoPayEnabled && (
                <div className={`settings-plan-status ${incomeType === "regular" ? "funded" : "planned"}`}>
                  {incomeType === "regular" 
                    ? "Auto-funded each paycheck until fully funded"
                    : "Prioritized in budget allocation"
                  }
                </div>
              )}
            </div>
          )})}
        </div>
      )}
      {deletePrompt && (
        <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
          <div className="card p-6 max-w-md space-y-4">
            <h3 className="text-lg font-semibold">Delete fixed expense?</h3>
            <p className="text-sm muted">
              Are you sure you want to delete {deletePrompt.name}? This action cannot be undone.
            </p>
            {!deletePrompt._isNew && (deletePrompt.fundedCents ?? 0) > 0 && (
              <div className="text-sm">
                Funded amount{" "}
                <span className="font-semibold">
                  <Money cents={deletePrompt.fundedCents ?? 0} />
                </span>{" "}
                will be refunded to your available budget.
              </div>
            )}
            <div className="flex gap-3 justify-end">
              <button className="btn" onClick={() => setDeletePrompt(null)}>
                Cancel
              </button>
              <button
                className="btn btn-primary"
                onClick={() => {
                  onDelete(deletePrompt.id);
                  setDeletePrompt(null);
                }}
              >
                Delete
              </button>
            </div>
          </div>
        </div>
      )}
      {hasChanges && (
        <div className="flex gap-3 pt-4 border-t border-[--color-border]">
          <button
            className="btn btn-primary"
            onClick={() => void onSave()}
            disabled={isSaving}
          >
            {isSaving ? "Saving..." : "Save Changes"}
          </button>
          <button className="btn" onClick={onCancel} disabled={isSaving}>
            Cancel
          </button>
        </div>
      )}
    </div>
  );
  }
);

export default PlansSettings;

// Inline editor components
function InlineEditText({
  value,
  onChange,
  placeholder,
}: {
  value: string;
  onChange: (v: string) => void;
  placeholder?: string;
}) {
  const [v, setV] = useState(value);
  const [editing, setEditing] = useState(false);

  useEffect(() => setV(value), [value]);

  const commit = () => {
    if (v !== value) onChange(v.trim());
    setEditing(false);
  };

  return editing ? (
    <input
      className="input"
      value={v}
      onChange={(e) => setV(e.target.value)}
      onBlur={commit}
      onKeyDown={(e) => e.key === "Enter" && commit()}
      placeholder={placeholder}
      autoFocus
    />
  ) : (
    <button
      type="button"
      className="link inline-flex items-center gap-1"
      onClick={() => setEditing(true)}
    >
      <span>{value || placeholder}</span>
      <span className="text-[10px] opacity-60">Edit</span>
    </button>
  );
}

function InlineEditMoney({
  cents,
  onChange,
  placeholder,
}: {
  cents: number;
  onChange: (cents: number) => void;
  placeholder?: string;
}) {
  const [editing, setEditing] = useState(false);
  const [v, setV] = useState((cents / 100).toFixed(2));

  useEffect(() => setV((cents / 100).toFixed(2)), [cents]);

  const commit = () => {
    const newCents = Math.max(0, parseCurrencyToCents(v));
    if (newCents !== cents) onChange(newCents);
    setEditing(false);
  };

  return editing ? (
    <input
      className="input w-24 text-right"
      value={v}
      onChange={(e) => setV(e.target.value)}
      onBlur={commit}
      onKeyDown={(e) => e.key === "Enter" && commit()}
      placeholder={placeholder}
      autoFocus
    />
  ) : (
    <button
      type="button"
      className="link"
      onClick={() => setEditing(true)}
    >
      <Money cents={cents} />
    </button>
  );
}

function InlineEditSelect({
  value,
  onChange,
}: {
  value: string;
  onChange: (value: string) => void;
}) {
  return (
    <select
      className="input w-32 text-sm"
      value={value}
      onChange={(e) => onChange(e.target.value)}
    >
      <option value="">Select</option>
      <option value="one-time">One-time</option>
      <option value="weekly">Weekly</option>
      <option value="biweekly">Biweekly</option>
      <option value="monthly">Monthly</option>
    </select>
  );
}

function InlineEditDate({
  value,
  timezone,
  onChange,
}: {
  value: string;
  timezone: string;
  onChange: (iso: string) => void;
}) {
  const [editing, setEditing] = useState(false);
  const [v, setV] = useState(isoToDateString(value, timezone));

  useEffect(() => setV(isoToDateString(value, timezone)), [value, timezone]);

  const commit = () => {
    if (v) {
      const nextISO = dateStringToUTCMidnight(v, timezone);
      if (nextISO !== value) onChange(nextISO);
    }
    setEditing(false);
  };

  return editing ? (
    <input
      className="input w-32"
      type="date"
      value={v}
      onChange={(e) => setV(e.target.value)}
      onBlur={commit}
      onKeyDown={(e) => e.key === "Enter" && commit()}
      autoFocus
    />
  ) : (
    <button
      type="button"
      className="link"
      onClick={() => setEditing(true)}
    >
      {formatDateInTimezone(value, timezone)}
    </button>
  );
}
