'use strict';

import type { Db } from '../types/db';

const { getDb } = require('../db/database.cts');
const { accountingActiveSql } = require('./paymentAccountingService.cts');
// Aggregation output re-typed by the client; branding adds friction (Dollars|null
// through many .filter/.sort/arith) without much safety here → plain require (any).
const { sumMoney, fromCents } = require('../utils/money.mts');
const { resolveDueDate } = require('./statusService.cts');

// Dynamic better-sqlite3 rows / request query.
type Row = Record<string, any>;

interface MonthInfo {
  year: number;
  month: number;
  key: string;
  label: string;
  start: string;
  end: string;
}
interface ParsedQuery {
  year: number;
  month: number;
  months: number;
  categoryId: number | null;
  billId: number | null;
  includeInactive: boolean;
  includeSkipped: boolean;
}
interface QueryError {
  error: string;
}
interface BillWhereOpts {
  userId: number;
  categoryId: number | null;
  billId: number | null;
  includeInactive: boolean;
}

function parseInteger(value: unknown, fallback: number | null): number | null {
  if (value === undefined || value === null || value === '') return fallback;
  const parsed = Number(value);
  return Number.isInteger(parsed) ? parsed : NaN;
}

function monthKey(year: number, month: number): string {
  return `${year}-${String(month).padStart(2, '0')}`;
}

function monthLabel(year: number, month: number): string {
  return new Date(Date.UTC(year, month - 1, 1)).toLocaleString('en-US', {
    month: 'short',
    year: '2-digit',
    timeZone: 'UTC',
  });
}

function addMonths(year: number, month: number, delta: number): { year: number; month: number } {
  const date = new Date(Date.UTC(year, month - 1 + delta, 1));
  return { year: date.getUTCFullYear(), month: date.getUTCMonth() + 1 };
}

function monthEndDate(year: number, month: number): string {
  const day = new Date(Date.UTC(year, month, 0)).getUTCDate();
  return `${monthKey(year, month)}-${String(day).padStart(2, '0')}`;
}

function buildMonths(endYear: number, endMonth: number, count: number): MonthInfo[] {
  return Array.from({ length: count }, (_, index) => {
    const value = addMonths(endYear, endMonth, index - count + 1);
    return {
      ...value,
      key: monthKey(value.year, value.month),
      label: monthLabel(value.year, value.month),
      start: `${monthKey(value.year, value.month)}-01`,
      end: monthEndDate(value.year, value.month),
    };
  });
}

function validateSummaryQuery(query: Row, now: Date = new Date()): ParsedQuery | QueryError {
  const year = parseInteger(query.year, now.getFullYear());
  const month = parseInteger(query.month, now.getMonth() + 1);
  const months = parseInteger(query.months, 12);
  const categoryId = parseInteger(query.category_id, null);
  const billId = parseInteger(query.bill_id, null);
  const includeInactive = query.include_inactive === 'true';
  const includeSkipped = query.include_skipped !== 'false';

  if (!Number.isInteger(year) || year! < 2000 || year! > 2100) {
    return { error: 'year must be a 4-digit integer between 2000 and 2100' };
  }
  if (!Number.isInteger(month) || month! < 1 || month! > 12) {
    return { error: 'month must be an integer between 1 and 12' };
  }
  if (!Number.isInteger(months) || months! < 1 || months! > 36) {
    return { error: 'months must be an integer between 1 and 36' };
  }
  if (categoryId !== null && (!Number.isInteger(categoryId) || categoryId < 1)) {
    return { error: 'category_id must be a positive integer' };
  }
  if (billId !== null && (!Number.isInteger(billId) || billId < 1)) {
    return { error: 'bill_id must be a positive integer' };
  }

  return {
    year,
    month,
    months,
    categoryId,
    billId,
    includeInactive,
    includeSkipped,
  } as ParsedQuery;
}

function isMonthInPast(year: number, month: number): boolean {
  const now = new Date();
  const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1);
  const targetMonthStart = new Date(year, month - 1, 1);
  return targetMonthStart < currentMonthStart;
}

function buildBillWhere({ userId, categoryId, billId, includeInactive }: BillWhereOpts): {
  where: string;
  params: any[];
} {
  const clauses = ['b.user_id = ?', 'b.deleted_at IS NULL'];
  const params: any[] = [userId];
  if (!includeInactive) clauses.push('b.active = 1');
  if (categoryId) {
    clauses.push('b.category_id = ?');
    params.push(categoryId);
  }
  if (billId) {
    clauses.push('b.id = ?');
    params.push(billId);
  }
  return { where: clauses.join(' AND '), params };
}

function emptySummary(
  parsed: ParsedQuery,
  rangeMonths: MonthInfo[],
  startDate: string,
  endDate: string,
  categories: any[],
) {
  return {
    range: {
      year: parsed.year,
      month: parsed.month,
      months: parsed.months,
      start: startDate,
      end: endDate,
    },
    filters: {
      category_id: parsed.categoryId,
      bill_id: parsed.billId,
      include_inactive: parsed.includeInactive,
      include_skipped: parsed.includeSkipped,
    },
    categories,
    bills: [],
    monthly_spending: [],
    expected_vs_actual: [],
    category_spend: [],
    heatmap: {
      months: rangeMonths.map(({ key, label, year, month }) => ({ key, label, year, month })),
      rows: [],
    },
    generated_at: new Date().toISOString(),
  };
}

function getAnalyticsSummary(userId: number, query: Row = {}) {
  const parsed = validateSummaryQuery(query);
  if ('error' in parsed) return parsed;

  const db: Db = getDb();
  const rangeMonths = buildMonths(parsed.year, parsed.month, parsed.months);
  const startDate = rangeMonths[0].start;
  const endDate = rangeMonths[rangeMonths.length - 1].end;
  const billWhere = buildBillWhere({ ...parsed, userId });

  const categories = db
    .prepare(
      `
    SELECT id, name
    FROM categories
    WHERE user_id = ?
      AND deleted_at IS NULL
    ORDER BY name COLLATE NOCASE
  `,
    )
    .all(userId);

  const bills = db
    .prepare(
      `
    SELECT b.id, b.name, b.category_id, b.expected_amount, b.active, b.created_at,
           b.due_day, b.billing_cycle, b.cycle_type, b.cycle_day,
           c.name AS category_name
    FROM bills b
    LEFT JOIN categories c ON c.id = b.category_id AND c.user_id = b.user_id AND c.deleted_at IS NULL
    WHERE ${billWhere.where}
    ORDER BY b.name COLLATE NOCASE
  `,
    )
    .all(...billWhere.params);

  if (!bills.length) {
    return emptySummary(parsed, rangeMonths, startDate, endDate, categories);
  }

  const billIds = bills.map((b: Row) => b.id);
  const placeholders = billIds.map(() => '?').join(',');

  const paymentRows = db
    .prepare(
      `
    SELECT p.bill_id,
           substr(p.paid_date, 1, 7) AS month_key,
           SUM(p.amount) AS total
    FROM payments p
    JOIN bills b ON b.id = p.bill_id
    WHERE b.user_id = ?
      AND b.deleted_at IS NULL
      AND p.bill_id IN (${placeholders})
      AND p.paid_date BETWEEN ? AND ?
      AND p.deleted_at IS NULL
      AND ${accountingActiveSql('p')}
    GROUP BY p.bill_id, substr(p.paid_date, 1, 7)
  `,
    )
    .all(userId, ...billIds, startDate, endDate);

  const stateRows = db
    .prepare(
      `
    SELECT m.bill_id, m.year, m.month, m.actual_amount, m.is_skipped
    FROM monthly_bill_state m
    JOIN bills b ON b.id = m.bill_id
    WHERE b.user_id = ?
      AND b.deleted_at IS NULL
      AND m.bill_id IN (${placeholders})
      AND (m.year * 100 + m.month) BETWEEN ? AND ?
  `,
    )
    .all(
      userId,
      ...billIds,
      rangeMonths[0].year * 100 + rangeMonths[0].month,
      rangeMonths[rangeMonths.length - 1].year * 100 + rangeMonths[rangeMonths.length - 1].month,
    );

  const paymentByBillMonth = new Map<string, number>(
    paymentRows.map((row: Row) => [`${row.bill_id}:${row.month_key}`, Number(row.total) || 0]),
  );
  const stateByBillMonth = new Map<string, any>(
    stateRows.map((row: Row) => [`${row.bill_id}:${monthKey(row.year, row.month)}`, row]),
  );

  const monthly_spending = rangeMonths
    .map((m) => {
      const total = sumMoney(
        bills,
        (bill: Row) => paymentByBillMonth.get(`${bill.id}:${m.key}`) || 0,
      );
      return { month: m.key, label: m.label, total: fromCents(total) };
    })
    .filter((row: Row) => row.total > 0);

  const expected_vs_actual = rangeMonths
    .map((m) => {
      const [mYear, mMonth] = m.key.split('-').map(Number);
      let expected = 0;
      let actual = 0;
      let skipped_count = 0;
      for (const bill of bills) {
        const state = stateByBillMonth.get(`${bill.id}:${m.key}`);
        const skipped = !!state?.is_skipped;
        if (skipped) skipped_count += 1;
        if (!skipped || parsed.includeSkipped) {
          actual += paymentByBillMonth.get(`${bill.id}:${m.key}`) || 0;
        }
        // QA-B5-01 family: only add "expected" in months the bill actually occurs,
        // so annual / off-month quarterly bills don't inflate the expected line
        // every month (matches the Tracker / Summary occurrence gate).
        if (!skipped && resolveDueDate(bill, mYear, mMonth)) {
          expected += state?.actual_amount ?? bill.expected_amount ?? 0;
        }
      }
      return {
        month: m.key,
        label: m.label,
        expected: fromCents(expected),
        actual: fromCents(actual),
        skipped_count,
      };
    })
    .filter((row: Row) => row.expected > 0 || row.actual > 0 || row.skipped_count > 0);

  const categoryMap = new Map<string, any>();
  for (const bill of bills) {
    const categoryId = bill.category_id || null;
    const key = categoryId == null ? 'uncategorized' : String(categoryId);
    const existing = categoryMap.get(key) || {
      category_id: categoryId,
      category_name: bill.category_name || 'Uncategorized',
      total: 0,
    };
    for (const m of rangeMonths) {
      existing.total += paymentByBillMonth.get(`${bill.id}:${m.key}`) || 0;
    }
    categoryMap.set(key, existing);
  }
  const category_spend = Array.from(categoryMap.values())
    .map((row) => ({ ...row, total: fromCents(row.total) }))
    .filter((row) => row.total > 0)
    .sort((a, b) => b.total - a.total);

  const heatmapRows = bills.map((bill: Row) => {
    const cells = rangeMonths.map((m) => {
      const paid = (paymentByBillMonth.get(`${bill.id}:${m.key}`) || 0) > 0;
      const state = stateByBillMonth.get(`${bill.id}:${m.key}`);
      const skipped = !!state?.is_skipped;
      let status = 'no_data';
      if (skipped) status = 'skipped';
      else if (paid) status = 'paid';
      else if (isMonthInPast(m.year, m.month)) status = 'missed';
      return {
        month: m.key,
        label: m.label,
        status,
        amount_paid: fromCents(paymentByBillMonth.get(`${bill.id}:${m.key}`) || 0),
      };
    });
    return {
      bill_id: bill.id,
      bill_name: bill.name,
      category_name: bill.category_name || 'Uncategorized',
      active: !!bill.active,
      cells: parsed.includeSkipped ? cells : cells.filter((cell) => cell.status !== 'skipped'),
    };
  });

  return {
    range: {
      year: parsed.year,
      month: parsed.month,
      months: parsed.months,
      start: startDate,
      end: endDate,
    },
    filters: {
      category_id: parsed.categoryId,
      bill_id: parsed.billId,
      include_inactive: parsed.includeInactive,
      include_skipped: parsed.includeSkipped,
    },
    categories,
    bills: bills.map((b: Row) => ({
      id: b.id,
      name: b.name,
      category_id: b.category_id,
      category_name: b.category_name || 'Uncategorized',
      active: !!b.active,
    })),
    monthly_spending,
    expected_vs_actual,
    category_spend,
    heatmap: {
      months: rangeMonths.map(({ key, label, year, month }) => ({ key, label, year, month })),
      rows: heatmapRows,
    },
    generated_at: new Date().toISOString(),
  };
}

module.exports = {
  addMonths,
  buildMonths,
  getAnalyticsSummary,
  monthEndDate,
  monthKey,
  monthLabel,
  validateSummaryQuery,
};
