import { getDb } from "./db";
import { inPlaceholders } from "../utils/sqlFilters";
import type {
  Category,
  BudgetEntry,
  BudgetTemplate,
  BudgetTemplateEntry,
  BudgetVsActualRow,
} from "../shared/types";

function computeMonthDateRange(year: number, month: number) {
  const dateFrom = `${year}-${String(month).padStart(2, "0")}-01`;
  const lastDay = new Date(year, month, 0).getDate();
  const dateTo = `${year}-${String(month).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}`;
  return { dateFrom, dateTo };
}

export async function getActiveCategories(): Promise<Category[]> {
  const db = await getDb();
  return db.select<Category[]>(
    "SELECT * FROM categories WHERE is_active = 1 AND is_inputable = 1 ORDER BY sort_order, name"
  );
}

export async function getAllActiveCategories(): Promise<Category[]> {
  const db = await getDb();
  return db.select<Category[]>(
    "SELECT * FROM categories WHERE is_active = 1 ORDER BY sort_order, name"
  );
}

export async function getBudgetEntriesForMonth(
  year: number,
  month: number
): Promise<BudgetEntry[]> {
  const db = await getDb();
  return db.select<BudgetEntry[]>(
    "SELECT * FROM budget_entries WHERE year = $1 AND month = $2",
    [year, month]
  );
}

export async function upsertBudgetEntry(
  categoryId: number,
  year: number,
  month: number,
  amount: number,
  notes?: string
): Promise<void> {
  const db = await getDb();
  await db.execute(
    `INSERT INTO budget_entries (category_id, year, month, amount, notes)
     VALUES ($1, $2, $3, $4, $5)
     ON CONFLICT(category_id, year, month) DO UPDATE SET amount = $4, notes = $5, updated_at = CURRENT_TIMESTAMP`,
    [categoryId, year, month, amount, notes || null]
  );
}

export async function deleteBudgetEntry(
  categoryId: number,
  year: number,
  month: number
): Promise<void> {
  const db = await getDb();
  await db.execute(
    "DELETE FROM budget_entries WHERE category_id = $1 AND year = $2 AND month = $3",
    [categoryId, year, month]
  );
}

export async function getActualsByCategory(
  year: number,
  month: number
): Promise<Array<{ category_id: number | null; actual: number }>> {
  const db = await getDb();
  const { dateFrom, dateTo } = computeMonthDateRange(year, month);
  return db.select<Array<{ category_id: number | null; actual: number }>>(
    `SELECT category_id, COALESCE(SUM(amount), 0) AS actual
     FROM transactions
     WHERE date BETWEEN $1 AND $2
     GROUP BY category_id`,
    [dateFrom, dateTo]
  );
}

export async function getBudgetEntriesForYear(
  year: number
): Promise<BudgetEntry[]> {
  const db = await getDb();
  return db.select<BudgetEntry[]>(
    "SELECT * FROM budget_entries WHERE year = $1",
    [year]
  );
}

export async function upsertBudgetEntriesForYear(
  categoryId: number,
  year: number,
  amounts: number[]
): Promise<void> {
  const db = await getDb();
  for (let m = 0; m < 12; m++) {
    const month = m + 1;
    const amount = amounts[m] ?? 0;
    if (amount === 0) {
      await db.execute(
        "DELETE FROM budget_entries WHERE category_id = $1 AND year = $2 AND month = $3",
        [categoryId, year, month]
      );
    } else {
      await db.execute(
        `INSERT INTO budget_entries (category_id, year, month, amount)
         VALUES ($1, $2, $3, $4)
         ON CONFLICT(category_id, year, month) DO UPDATE SET amount = $4, updated_at = CURRENT_TIMESTAMP`,
        [categoryId, year, month, amount]
      );
    }
  }
}

// Templates

export async function getAllTemplates(): Promise<BudgetTemplate[]> {
  const db = await getDb();
  return db.select<BudgetTemplate[]>(
    "SELECT * FROM budget_templates ORDER BY name"
  );
}

export async function getTemplateEntries(
  templateId: number
): Promise<BudgetTemplateEntry[]> {
  const db = await getDb();
  return db.select<BudgetTemplateEntry[]>(
    "SELECT * FROM budget_template_entries WHERE template_id = $1",
    [templateId]
  );
}

export async function saveAsTemplate(
  name: string,
  description: string | undefined,
  entries: Array<{ category_id: number; amount: number }>
): Promise<number> {
  const db = await getDb();
  const result = await db.execute(
    "INSERT INTO budget_templates (name, description) VALUES ($1, $2)",
    [name, description || null]
  );
  const templateId = result.lastInsertId as number;

  for (const entry of entries) {
    await db.execute(
      "INSERT INTO budget_template_entries (template_id, category_id, amount) VALUES ($1, $2, $3)",
      [templateId, entry.category_id, entry.amount]
    );
  }

  return templateId;
}

export async function applyTemplate(
  templateId: number,
  year: number,
  month: number
): Promise<void> {
  const entries = await getTemplateEntries(templateId);
  for (const entry of entries) {
    await upsertBudgetEntry(entry.category_id, year, month, entry.amount);
  }
}

export async function deleteTemplate(templateId: number): Promise<void> {
  const db = await getDb();
  await db.execute(
    "DELETE FROM budget_template_entries WHERE template_id = $1",
    [templateId]
  );
  await db.execute("DELETE FROM budget_templates WHERE id = $1", [templateId]);
}

// --- Actuals helpers ---

export async function getActualTotalsForYear(
  year: number,
  accountIds?: number[]
): Promise<Array<{ category_id: number | null; actual: number }>> {
  const dateFrom = `${year}-01-01`;
  const dateTo = `${year}-12-31`;
  return getActualsByCategoryRange(dateFrom, dateTo, accountIds);
}

// --- Budget vs Actual ---

async function getActualsByCategoryRange(
  dateFrom: string,
  dateTo: string,
  accountIds?: number[]
): Promise<Array<{ category_id: number | null; actual: number }>> {
  const db = await getDb();
  const params: unknown[] = [dateFrom, dateTo];
  const accountPlaceholders = inPlaceholders(accountIds, params.length + 1);
  const accountFilter = accountPlaceholders ? ` AND source_id IN (${accountPlaceholders})` : "";
  if (accountPlaceholders) params.push(...accountIds!);
  return db.select<Array<{ category_id: number | null; actual: number }>>(
    `SELECT category_id, COALESCE(SUM(amount), 0) AS actual
     FROM transactions
     WHERE date BETWEEN $1 AND $2${accountFilter}
     GROUP BY category_id`,
    params
  );
}

// Income-statement reading order: revenue first, then expenses, then
// transfers (Issue #277 — matches COMPARE_TYPE_ORDER / OVER_TIME_TYPE_ORDER).
const TYPE_ORDER: Record<string, number> = { income: 0, expense: 1, transfer: 2 };

/**
 * Budget vs actual, by category, for the reference month plus YTD. `accountIds`
 * (Issue #273) optionally scopes both the month and YTD actuals to a subset of
 * import sources (`transactions.source_id`) — budgeted amounts are unaffected
 * by the filter (a budget is not tied to any account).
 */
export async function getBudgetVsActualData(
  year: number,
  month: number,
  accountIds?: number[]
): Promise<BudgetVsActualRow[]> {
  // Date ranges
  const { dateFrom: monthFrom, dateTo: monthTo } = computeMonthDateRange(year, month);
  const ytdFrom = `${year}-01-01`;
  const ytdTo = monthTo;

  // Fetch all data in parallel
  const [allCategories, yearEntries, monthActuals, ytdActuals] = await Promise.all([
    getAllActiveCategories(),
    getBudgetEntriesForYear(year),
    getActualsByCategoryRange(monthFrom, monthTo, accountIds),
    getActualsByCategoryRange(ytdFrom, ytdTo, accountIds),
  ]);

  // Build maps
  const entryMap = new Map<number, Map<number, number>>();
  for (const e of yearEntries) {
    if (!entryMap.has(e.category_id)) entryMap.set(e.category_id, new Map());
    entryMap.get(e.category_id)!.set(e.month, e.amount);
  }

  const monthActualMap = new Map<number, number>();
  for (const a of monthActuals) {
    if (a.category_id != null) monthActualMap.set(a.category_id, a.actual);
  }

  const ytdActualMap = new Map<number, number>();
  for (const a of ytdActuals) {
    if (a.category_id != null) ytdActualMap.set(a.category_id, a.actual);
  }

  // Index categories
  const childrenByParent = new Map<number, Category[]>();
  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);
    }
  }

  // Sign multiplier: budget stored positive, expenses displayed negative
  const signFor = (type: string) => (type === "expense" ? -1 : 1);

  // Compute leaf row values
  function buildLeaf(cat: Category, parentId: number | null, depth: number): BudgetVsActualRow {
    const sign = signFor(cat.type);
    const monthMap = entryMap.get(cat.id);
    const rawMonthBudget = monthMap?.get(month) ?? 0;
    const monthBudget = rawMonthBudget * sign;

    let rawYtdBudget = 0;
    for (let m = 1; m <= month; m++) {
      rawYtdBudget += monthMap?.get(m) ?? 0;
    }
    const ytdBudget = rawYtdBudget * sign;

    const monthActual = monthActualMap.get(cat.id) ?? 0;
    const ytdActual = ytdActualMap.get(cat.id) ?? 0;

    const monthVariation = monthActual - monthBudget;
    const ytdVariation = ytdActual - ytdBudget;

    return {
      category_id: cat.id,
      category_name: cat.name,
      category_color: cat.color || "#9ca3af",
      category_type: cat.type,
      parent_id: parentId,
      is_parent: false,
      depth,
      monthActual,
      monthBudget,
      monthVariation,
      monthVariationPct: monthBudget !== 0 ? monthVariation / Math.abs(monthBudget) : null,
      ytdActual,
      ytdBudget,
      ytdVariation,
      ytdVariationPct: ytdBudget !== 0 ? ytdVariation / Math.abs(ytdBudget) : null,
    };
  }

  function buildSubtotal(cat: Category, childRows: BudgetVsActualRow[], parentId: number | null, depth: number): BudgetVsActualRow {
    const row: BudgetVsActualRow = {
      category_id: cat.id,
      category_name: cat.name,
      category_color: cat.color || "#9ca3af",
      category_type: cat.type,
      parent_id: parentId,
      is_parent: true,
      depth,
      monthActual: 0,
      monthBudget: 0,
      monthVariation: 0,
      monthVariationPct: null,
      ytdActual: 0,
      ytdBudget: 0,
      ytdVariation: 0,
      ytdVariationPct: null,
    };
    for (const cr of childRows) {
      row.monthActual += cr.monthActual;
      row.monthBudget += cr.monthBudget;
      row.monthVariation += cr.monthVariation;
      row.ytdActual += cr.ytdActual;
      row.ytdBudget += cr.ytdBudget;
      row.ytdVariation += cr.ytdVariation;
    }
    row.monthVariationPct =
      row.monthBudget !== 0 ? row.monthVariation / Math.abs(row.monthBudget) : null;
    row.ytdVariationPct =
      row.ytdBudget !== 0 ? row.ytdVariation / Math.abs(row.ytdBudget) : null;
    return row;
  }

  function isRowAllZero(r: BudgetVsActualRow): boolean {
    return (
      r.monthActual === 0 &&
      r.monthBudget === 0 &&
      r.ytdActual === 0 &&
      r.ytdBudget === 0
    );
  }

  // Build rows for a sub-group (recursive, supports arbitrary depth)
  function buildSubGroup(cat: Category, groupParentId: number, depth: number): BudgetVsActualRow[] {
    const subChildren = childrenByParent.get(cat.id) || [];
    const hasSubChildren = subChildren.some(
      (c) => c.is_inputable || (childrenByParent.get(c.id) || []).length > 0
    );

    if (!hasSubChildren && cat.is_inputable) {
      const leaf = buildLeaf(cat, groupParentId, depth);
      return isRowAllZero(leaf) ? [] : [leaf];
    }
    if (!hasSubChildren) return [];

    const childRows: BudgetVsActualRow[] = [];
    if (cat.is_inputable) {
      const direct = buildLeaf(cat, cat.id, depth + 1);
      direct.category_name = `${cat.name} (direct)`;
      if (!isRowAllZero(direct)) childRows.push(direct);
    }
    const sortedSubChildren = [...subChildren].sort((a, b) => a.name.localeCompare(b.name));
    for (const child of sortedSubChildren) {
      const grandchildren = childrenByParent.get(child.id) || [];
      if (grandchildren.length > 0) {
        const subRows = buildSubGroup(child, cat.id, depth + 1);
        childRows.push(...subRows);
      } else if (child.is_inputable) {
        const leaf = buildLeaf(child, cat.id, depth + 1);
        if (!isRowAllZero(leaf)) childRows.push(leaf);
      }
    }
    if (childRows.length === 0) return [];

    const leafRows = childRows.filter((r) => !r.is_parent);
    const subtotal = buildSubtotal(cat, leafRows, groupParentId, depth);
    return [subtotal, ...childRows];
  }

  const rows: BudgetVsActualRow[] = [];
  const topLevel = allCategories.filter((c) => !c.parent_id);

  for (const cat of topLevel) {
    const children = childrenByParent.get(cat.id) || [];
    const hasChildren = children.some(
      (c) => c.is_inputable || (childrenByParent.get(c.id) || []).length > 0
    );

    if (!hasChildren && cat.is_inputable) {
      // Standalone leaf at level 0
      const leaf = buildLeaf(cat, null, 0);
      if (!isRowAllZero(leaf)) rows.push(leaf);
    } else if (hasChildren) {
      const allChildRows: BudgetVsActualRow[] = [];

      // Direct transactions on the parent itself
      if (cat.is_inputable) {
        const direct = buildLeaf(cat, cat.id, 1);
        direct.category_name = `${cat.name} (direct)`;
        if (!isRowAllZero(direct)) allChildRows.push(direct);
      }

      // Process children in alphabetical order
      const sortedChildren = [...children].sort((a, b) => a.name.localeCompare(b.name));
      for (const child of sortedChildren) {
        const grandchildren = childrenByParent.get(child.id) || [];
        if (grandchildren.length > 0) {
          const subRows = buildSubGroup(child, cat.id, 1);
          allChildRows.push(...subRows);
        } else if (child.is_inputable) {
          const leaf = buildLeaf(child, cat.id, 1);
          if (!isRowAllZero(leaf)) allChildRows.push(leaf);
        }
      }

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

      // Collect only leaf rows for parent subtotal (avoid double-counting)
      const leafRows = allChildRows.filter((r) => !r.is_parent);
      const parent = buildSubtotal(cat, leafRows, null, 0);

      rows.push(parent);
      rows.push(...allChildRows);
    }
  }

  // Sort by type only, preserving tree order within groups (already built correctly)
  const rowOrder = new Map<BudgetVsActualRow, number>();
  rows.forEach((r, i) => rowOrder.set(r, i));

  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;
    return rowOrder.get(a)! - rowOrder.get(b)!;
  });

  return rows;
}
