import { useReducer, useCallback, useEffect, useRef } from "react";
import type { BudgetYearRow, BudgetTemplate, ImportSource, Category, BudgetEntry } from "../shared/types";
import {
  getAllActiveCategories,
  getBudgetEntriesForYear,
  getActualTotalsForYear,
  upsertBudgetEntry,
  upsertBudgetEntriesForYear,
  getAllTemplates,
  saveAsTemplate as saveAsTemplateSvc,
  applyTemplate as applyTemplateSvc,
  deleteTemplate as deleteTemplateSvc,
} from "../services/budgetService";
import { getAllImportSources } from "../services/transactionService";
import { useReportsPeriod } from "./useReportsPeriod";

interface BudgetState {
  year: number;
  rows: BudgetYearRow[];
  templates: BudgetTemplate[];
  accounts: ImportSource[];
  isLoading: boolean;
  isSaving: boolean;
  error: string | null;
}

type BudgetAction =
  | { type: "SET_LOADING"; payload: boolean }
  | { type: "SET_SAVING"; payload: boolean }
  | { type: "SET_ERROR"; payload: string | null }
  | { type: "SET_DATA"; payload: { rows: BudgetYearRow[]; templates: BudgetTemplate[] } }
  | { type: "SET_ACCOUNTS"; payload: ImportSource[] }
  | { type: "SET_YEAR"; payload: number };

function initialState(): BudgetState {
  return {
    year: new Date().getFullYear(),
    rows: [],
    templates: [],
    accounts: [],
    isLoading: false,
    isSaving: false,
    error: null,
  };
}

function reducer(state: BudgetState, action: BudgetAction): BudgetState {
  switch (action.type) {
    case "SET_LOADING":
      return { ...state, isLoading: action.payload };
    case "SET_SAVING":
      return { ...state, isSaving: action.payload };
    case "SET_ERROR":
      return { ...state, error: action.payload, isLoading: false, isSaving: false };
    case "SET_DATA":
      return {
        ...state,
        rows: action.payload.rows,
        templates: action.payload.templates,
        isLoading: false,
      };
    case "SET_ACCOUNTS":
      return { ...state, accounts: action.payload };
    case "SET_YEAR":
      return { ...state, year: action.payload };
    default:
      return state;
  }
}

// Income-statement reading order: revenue first, then expenses, then
// transfers (Issue #278) — matches the compare/trend reports' ordering
// (`COMPARE_TYPE_ORDER` / `OVER_TIME_TYPE_ORDER`, income-first since #253).
const TYPE_ORDER: Record<string, number> = { income: 0, expense: 1, transfer: 2 };

/**
 * Assembles the flat, hierarchical `BudgetYearRow[]` the grid renders from the
 * raw category tree, this year's budget entries, and last year's actual totals.
 *
 * ORDERING INVARIANT (issue #289 — the multi-level-collapse consumer). The
 * final `rows.sort` below orders each top category group by DEPTH ASCENDING
 * (level-order / BFS), NOT depth-first: within a group every depth-0 row
 * precedes every depth-1 row, which precedes every depth-2 row. The abandoned
 * v1 collapse algorithm wrongly assumed a depth-first emission order and broke
 * exactly here; the shipped algorithm (ancestor walk — see `collapsibleRows.ts`)
 * is order-INDEPENDENT, so this sort needs NO change. The level-order property
 * is pinned by `useBudget.test.ts` so a refactor can't silently reintroduce the
 * v1 assumption — do not reorder into DFS without revisiting the collapse
 * consumer (`BudgetTable`).
 *
 * Pure (no DB, no React) so the invariant is unit-testable without a renderer —
 * the repo has no jsdom, so hook logic that must be tested is extracted here,
 * the same way `useCompare.ts` exposes its pure helpers.
 */
export function buildBudgetYearRows(
  allCategories: Category[],
  entries: BudgetEntry[],
  prevYearActuals: Array<{ category_id: number | null; actual: number }>,
): BudgetYearRow[] {
  // Build a map: categoryId -> month(1-12) -> amount
  const entryMap = new Map<number, Map<number, number>>();
  for (const e of entries) {
    if (!entryMap.has(e.category_id)) entryMap.set(e.category_id, new Map());
    entryMap.get(e.category_id)!.set(e.month, e.amount);
  }

  // Build a map for previous year actuals: categoryId -> annual actual total
  // Amounts are already signed (expenses negative, income positive) — stored as-is.
  const prevYearTotalMap = new Map<number, number>();
  for (const a of prevYearActuals) {
    if (a.category_id != null) prevYearTotalMap.set(a.category_id, a.actual);
  }

  // Helper: build months array from entryMap
  const buildMonths = (catId: number) => {
    const monthMap = entryMap.get(catId);
    const months: number[] = [];
    let annual = 0;
    for (let m = 1; m <= 12; m++) {
      const val = monthMap?.get(m) ?? 0;
      months.push(val);
      annual += val;
    }
    const previousYearTotal = prevYearTotalMap.get(catId) ?? 0;
    return { months, annual, previousYearTotal };
  };

  // Index categories by id and group children by parent_id
  const catById = new Map(allCategories.map((c) => [c.id, c]));
  const childrenByParent = new Map<number, typeof allCategories>();
  for (const cat of allCategories) {
    if (cat.parent_id) {
      if (!childrenByParent.has(cat.parent_id)) childrenByParent.set(cat.parent_id, []);
      childrenByParent.get(cat.parent_id)!.push(cat);
    }
  }

  const rows: BudgetYearRow[] = [];

  // Build rows for an intermediate parent (level 1 or 2 with children)
  function buildLevel2Group(cat: typeof allCategories[0], grandparentId: number): BudgetYearRow[] {
    const grandchildren = (childrenByParent.get(cat.id) || []).filter((c) => c.is_inputable);
    if (grandchildren.length === 0 && cat.is_inputable) {
      // Leaf at depth 2
      const { months, annual, previousYearTotal } = buildMonths(cat.id);
      return [{
        category_id: cat.id,
        category_name: cat.name,
        category_color: cat.color || "#9ca3af",
        category_type: cat.type,
        parent_id: grandparentId,
        is_parent: false,
        depth: 2,
        months,
        annual,
        previousYearTotal,
      }];
    }
    if (grandchildren.length === 0 && !cat.is_inputable) {
      // Also check if it has non-inputable intermediate children with their own children
      // This shouldn't happen at depth 3 (max 3 levels), but handle gracefully
      return [];
    }

    const gcRows: BudgetYearRow[] = [];
    if (cat.is_inputable) {
      const { months, annual, previousYearTotal } = buildMonths(cat.id);
      gcRows.push({
        category_id: cat.id,
        category_name: `${cat.name} (direct)`,
        category_color: cat.color || "#9ca3af",
        category_type: cat.type,
        parent_id: cat.id,
        is_parent: false,
        depth: 2,
        months,
        annual,
        previousYearTotal,
      });
    }
    for (const gc of grandchildren) {
      const { months, annual, previousYearTotal } = buildMonths(gc.id);
      gcRows.push({
        category_id: gc.id,
        category_name: gc.name,
        category_color: gc.color || cat.color || "#9ca3af",
        category_type: gc.type,
        parent_id: cat.id,
        is_parent: false,
        depth: 2,
        months,
        annual,
        previousYearTotal,
      });
    }
    if (gcRows.length === 0) return [];

    // Build intermediate subtotal
    const subMonths = Array(12).fill(0) as number[];
    let subAnnual = 0;
    let subPrevYear = 0;
    for (const cr of gcRows) {
      for (let m = 0; m < 12; m++) subMonths[m] += cr.months[m];
      subAnnual += cr.annual;
      subPrevYear += cr.previousYearTotal;
    }
    const subtotal: BudgetYearRow = {
      category_id: cat.id,
      category_name: cat.name,
      category_color: cat.color || "#9ca3af",
      category_type: cat.type,
      parent_id: grandparentId,
      is_parent: true,
      depth: 1,
      months: subMonths,
      annual: subAnnual,
      previousYearTotal: subPrevYear,
    };
    gcRows.sort((a, b) => {
      if (a.category_id === cat.id) return -1;
      if (b.category_id === cat.id) return 1;
      return a.category_name.localeCompare(b.category_name);
    });
    return [subtotal, ...gcRows];
  }

  // Identify top-level parents and standalone leaves
  const topLevel = allCategories.filter((c) => !c.parent_id);

  for (const cat of topLevel) {
    const children = childrenByParent.get(cat.id) || [];
    const inputableChildren = children.filter((c) => c.is_inputable);
    const intermediateParents = children.filter((c) => !c.is_inputable && (childrenByParent.get(c.id) || []).length > 0);

    if (inputableChildren.length === 0 && intermediateParents.length === 0 && cat.is_inputable) {
      // Standalone leaf (no children) — regular editable row
      const { months, annual, previousYearTotal } = buildMonths(cat.id);
      rows.push({
        category_id: cat.id,
        category_name: cat.name,
        category_color: cat.color || "#9ca3af",
        category_type: cat.type,
        parent_id: null,
        is_parent: false,
        depth: 0,
        months,
        annual,
        previousYearTotal,
      });
    } else if (inputableChildren.length > 0 || intermediateParents.length > 0) {
      const allChildRows: BudgetYearRow[] = [];

      // If parent is also inputable, create a "(direct)" fake-child row
      if (cat.is_inputable) {
        const { months, annual, previousYearTotal } = buildMonths(cat.id);
        allChildRows.push({
          category_id: cat.id,
          category_name: `${cat.name} (direct)`,
          category_color: cat.color || "#9ca3af",
          category_type: cat.type,
          parent_id: cat.id,
          is_parent: false,
          depth: 1,
          months,
          annual,
          previousYearTotal,
        });
      }

      for (const child of inputableChildren) {
        const grandchildren = childrenByParent.get(child.id) || [];
        if (grandchildren.length === 0) {
          // Simple leaf at depth 1
          const { months, annual, previousYearTotal } = buildMonths(child.id);
          allChildRows.push({
            category_id: child.id,
            category_name: child.name,
            category_color: child.color || cat.color || "#9ca3af",
            category_type: child.type,
            parent_id: cat.id,
            is_parent: false,
            depth: 1,
            months,
            annual,
            previousYearTotal,
          });
        } else {
          // Intermediate parent at depth 1 with grandchildren
          allChildRows.push(...buildLevel2Group(child, cat.id));
        }
      }

      // Non-inputable intermediate parents
      for (const ip of intermediateParents) {
        allChildRows.push(...buildLevel2Group(ip, cat.id));
      }

      if (allChildRows.length === 0) continue;

      // Parent subtotal row: sum of leaf rows only (avoid double-counting)
      const leafRows = allChildRows.filter((r) => !r.is_parent);
      const parentMonths = Array(12).fill(0) as number[];
      let parentAnnual = 0;
      let parentPrevYear = 0;
      for (const cr of leafRows) {
        for (let m = 0; m < 12; m++) parentMonths[m] += cr.months[m];
        parentAnnual += cr.annual;
        parentPrevYear += cr.previousYearTotal;
      }

      rows.push({
        category_id: cat.id,
        category_name: cat.name,
        category_color: cat.color || "#9ca3af",
        category_type: cat.type,
        parent_id: null,
        is_parent: true,
        depth: 0,
        months: parentMonths,
        annual: parentAnnual,
        previousYearTotal: parentPrevYear,
      });

      // Sort children alphabetically, but keep "(direct)" first
      allChildRows.sort((a, b) => {
        if (a.category_id === cat.id && !a.is_parent) return -1;
        if (b.category_id === cat.id && !b.is_parent) return 1;
        return a.category_name.localeCompare(b.category_name);
      });

      rows.push(...allChildRows);
    }
    // else: non-inputable parent with no inputable children — skip
  }

  // Sort by type, then within each type: keep hierarchy groups together
  function getTopGroupId(r: BudgetYearRow): number {
    if ((r.depth ?? 0) === 0) return r.category_id;
    if (r.is_parent && r.parent_id === null) return r.category_id;
    let pid = r.parent_id;
    while (pid !== null) {
      const pCat = catById.get(pid);
      if (!pCat || !pCat.parent_id) return pid;
      pid = pCat.parent_id;
    }
    return r.category_id;
  }

  rows.sort((a, b) => {
    const typeA = TYPE_ORDER[a.category_type] ?? 9;
    const typeB = TYPE_ORDER[b.category_type] ?? 9;
    if (typeA !== typeB) return typeA - typeB;
    const groupA = getTopGroupId(a);
    const groupB = getTopGroupId(b);
    if (groupA !== groupB) {
      const catA = catById.get(groupA);
      const catB = catById.get(groupB);
      const orderA = catA?.sort_order ?? 999;
      const orderB = catB?.sort_order ?? 999;
      if (orderA !== orderB) return orderA - orderB;
      return (catA?.name ?? "").localeCompare(catB?.name ?? "");
    }
    // Same group: sort by depth, then parent before children at same depth
    if (a.is_parent !== b.is_parent && (a.depth ?? 0) === (b.depth ?? 0)) return a.is_parent ? -1 : 1;
    if ((a.depth ?? 0) !== (b.depth ?? 0)) return (a.depth ?? 0) - (b.depth ?? 0);
    if (a.parent_id && a.category_id === a.parent_id) return -1;
    if (b.parent_id && b.category_id === b.parent_id) return 1;
    return a.category_name.localeCompare(b.category_name);
  });

  return rows;
}

export function useBudget() {
  const { accountIds } = useReportsPeriod();
  const [state, dispatch] = useReducer(reducer, undefined, initialState);
  const fetchIdRef = useRef(0);

  const refreshData = useCallback(async (year: number, ids: number[]) => {
    const fetchId = ++fetchIdRef.current;
    dispatch({ type: "SET_LOADING", payload: true });
    dispatch({ type: "SET_ERROR", payload: null });

    try {
      const [allCategories, entries, prevYearActuals, templates] = await Promise.all([
        getAllActiveCategories(),
        getBudgetEntriesForYear(year),
        getActualTotalsForYear(year - 1, ids),
        getAllTemplates(),
      ]);

      if (fetchId !== fetchIdRef.current) return;

      const rows = buildBudgetYearRows(allCategories, entries, prevYearActuals);

      dispatch({ type: "SET_DATA", payload: { rows, templates } });
    } catch (e) {
      if (fetchId !== fetchIdRef.current) return;
      dispatch({
        type: "SET_ERROR",
        payload: e instanceof Error ? e.message : String(e),
      });
    }
  }, []);

  useEffect(() => {
    refreshData(state.year, accountIds);
  }, [state.year, accountIds, refreshData]);

  // Load the import-source list once, for the FilterPanel's account checkboxes
  // (same query as the Trends/Compare source filters — see useTrends.ts).
  useEffect(() => {
    (async () => {
      try {
        const accounts = await getAllImportSources();
        dispatch({ type: "SET_ACCOUNTS", payload: accounts });
      } catch (e) {
        dispatch({ type: "SET_ERROR", payload: e instanceof Error ? e.message : String(e) });
      }
    })();
  }, []);

  const navigateYear = useCallback((delta: -1 | 1) => {
    dispatch({ type: "SET_YEAR", payload: state.year + delta });
  }, [state.year]);

  const updatePlanned = useCallback(
    async (categoryId: number, month: number, amount: number) => {
      dispatch({ type: "SET_SAVING", payload: true });
      try {
        await upsertBudgetEntry(categoryId, state.year, month, amount);
        await refreshData(state.year, accountIds);
      } catch (e) {
        dispatch({
          type: "SET_ERROR",
          payload: e instanceof Error ? e.message : String(e),
        });
      } finally {
        dispatch({ type: "SET_SAVING", payload: false });
      }
    },
    [state.year, accountIds, refreshData]
  );

  const splitEvenly = useCallback(
    async (categoryId: number, annualAmount: number) => {
      dispatch({ type: "SET_SAVING", payload: true });
      try {
        const base = Math.floor((annualAmount / 12) * 100) / 100;
        const remainder = Math.round((annualAmount - base * 12) * 100);
        const amounts: number[] = [];
        for (let m = 0; m < 12; m++) {
          amounts.push(m < remainder ? base + 0.01 : base);
        }
        await upsertBudgetEntriesForYear(categoryId, state.year, amounts);
        await refreshData(state.year, accountIds);
      } catch (e) {
        dispatch({
          type: "SET_ERROR",
          payload: e instanceof Error ? e.message : String(e),
        });
      } finally {
        dispatch({ type: "SET_SAVING", payload: false });
      }
    },
    [state.year, accountIds, refreshData]
  );

  const saveTemplate = useCallback(
    async (name: string, description?: string) => {
      dispatch({ type: "SET_SAVING", payload: true });
      try {
        // Save template from January values (template is a single-month snapshot)
        // Exclude parent subtotal rows (they're computed, not real entries)
        const entries = state.rows
          .filter((r) => !r.is_parent && r.months[0] !== 0)
          .map((r) => ({ category_id: r.category_id, amount: r.months[0] }));
        await saveAsTemplateSvc(name, description, entries);
        await refreshData(state.year, accountIds);
      } catch (e) {
        dispatch({
          type: "SET_ERROR",
          payload: e instanceof Error ? e.message : String(e),
        });
      } finally {
        dispatch({ type: "SET_SAVING", payload: false });
      }
    },
    [state.rows, state.year, accountIds, refreshData]
  );

  const applyTemplate = useCallback(
    async (templateId: number, month: number) => {
      dispatch({ type: "SET_SAVING", payload: true });
      try {
        await applyTemplateSvc(templateId, state.year, month);
        await refreshData(state.year, accountIds);
      } catch (e) {
        dispatch({
          type: "SET_ERROR",
          payload: e instanceof Error ? e.message : String(e),
        });
      } finally {
        dispatch({ type: "SET_SAVING", payload: false });
      }
    },
    [state.year, accountIds, refreshData]
  );

  const applyTemplateAllMonths = useCallback(
    async (templateId: number) => {
      dispatch({ type: "SET_SAVING", payload: true });
      try {
        for (let m = 1; m <= 12; m++) {
          await applyTemplateSvc(templateId, state.year, m);
        }
        await refreshData(state.year, accountIds);
      } catch (e) {
        dispatch({
          type: "SET_ERROR",
          payload: e instanceof Error ? e.message : String(e),
        });
      } finally {
        dispatch({ type: "SET_SAVING", payload: false });
      }
    },
    [state.year, accountIds, refreshData]
  );

  const deleteTemplate = useCallback(
    async (templateId: number) => {
      dispatch({ type: "SET_SAVING", payload: true });
      try {
        await deleteTemplateSvc(templateId);
        await refreshData(state.year, accountIds);
      } catch (e) {
        dispatch({
          type: "SET_ERROR",
          payload: e instanceof Error ? e.message : String(e),
        });
      } finally {
        dispatch({ type: "SET_SAVING", payload: false });
      }
    },
    [state.year, accountIds, refreshData]
  );

  return {
    state,
    navigateYear,
    updatePlanned,
    splitEvenly,
    saveTemplate,
    applyTemplate,
    applyTemplateAllMonths,
    deleteTemplate,
  };
}
