import { DateTime } from 'luxon';
import { MULTIPLIER_BY_YEAR, round2 } from '../../constants/schemeYearMultiplier';
import { resolveCurrentOmDisplayYearForPendingBill } from './resolveOmBillDisplayYear';

/** Invoice fields needed for O&M 10-year aggregation (subset of Invoice entity). */
export type OmTenYearInvoiceInput = {
  fromDate?: string | null;
  toDate?: string | null;
  billingPeriodKey?: string | null;
  penaltyAmount?: number | null;
  /** Column M: sum for invoices overlapping this O&M year. */
  paidAmount?: number | null;
};

export type OmTenYearRowView = {
  year: number;
  /** Display: e.g. "1st year of O&M" */
  yearLabel: string;
  start: string;
  end: string;
  percent: string;
  amount: string;
  monthly: string;
  from: string;
  to: string;
  months: string;
  /** J */
  totalBill: string;
  /** K */
  cumulative: string;
  /** L */
  penalty: string;
  /** M: Bill paid till date (in Rs) */
  billPaid: string;
  /** N */
  finalBasic: string;
  /** O */
  gst18: string;
  /** P */
  totalPayable: string;
  /** Q */
  gstDed2: string;
  /** R */
  tdsDed2: string;
  /** S */
  labourCess: string;
  /** T */
  totalDeduction: string;
  /** U */
  finalAmount: string;
};

function formatDdMmYyyy(dt: DateTime): string {
  return dt.isValid ? dt.toFormat('dd/LL/yyyy') : '';
}

function formatMoney(n: number): string {
  if (!Number.isFinite(n)) return '';
  return round2(n).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}

function formatPercentFromMultiplier(mult: number): string {
  const pct = mult * 100;
  return `${pct.toFixed(3)}%`;
}

/** e.g. 1 -> "1st year of O&M", 10 -> "10th year of O&M" */
export function formatOmDisplayYearLabel(displayYear1To10: number): string {
  const y = Math.trunc(displayYear1To10);
  const ord: Record<number, string> = {
    1: '1st',
    2: '2nd',
    3: '3rd',
    4: '4th',
    5: '5th',
    6: '6th',
    7: '7th',
    8: '8th',
    9: '9th',
    10: '10th',
  };
  const p = ord[y] || `${y}th`;
  return `${p} year of O&M`;
}

/**
 * Per-row J–U from I, F, and penalty L (column).
 * J = I * F, N = J - L, O = N * 18%, P = N + O, Q/R/S = N*2%/2%/1%, T = Q+R+S, U = P - T.
 */
export function computeRowFinancials(params: { I: number; F: number; L: number }): {
  J: number;
  N: number;
  O: number;
  P: number;
  Q: number;
  R: number;
  S: number;
  T: number;
  U: number;
} {
  const { I, F, L } = params;
  const J = round2(I * F);
  const N = round2(J - L);
  const O = round2(N * 0.18);
  const P = round2(N + O);
  const Q = round2(N * 0.02);
  const R = round2(N * 0.02);
  const S = round2(N * 0.01);
  const T = round2(Q + R + S);
  const U = round2(P - T);
  return { J, N, O, P, Q, R, S, T, U };
}

function parseDateOnly(iso: string, zone: string): DateTime | null {
  const s = String(iso || '').trim().slice(0, 10);
  if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return null;
  const d = DateTime.fromISO(s, { zone });
  return d.isValid ? d.startOf('day') : null;
}

/**
 * Wall-calendar effective [from, to] for an invoice; used for overlap with O&M year windows.
 */
export function effectiveInvoiceRange(inv: OmTenYearInvoiceInput, zone: string): { from: DateTime; to: DateTime } | null {
  const fromRaw = inv.fromDate?.trim() || inv.billingPeriodKey?.trim();
  if (!fromRaw) return null;
  const from = parseDateOnly(fromRaw, zone);
  if (!from) return null;
  const toRaw = inv.toDate?.trim();
  if (toRaw) {
    const to = parseDateOnly(toRaw, zone);
    if (to) return { from, to: to < from ? from : to };
  }
  return { from, to: from };
}

function rangesOverlap(
  aFrom: DateTime,
  aTo: DateTime,
  bFrom: DateTime,
  bTo: DateTime
): boolean {
  return aFrom <= bTo && aTo >= bFrom;
}

/** Latest bill = max end date (`to`), tie-break by max start (`from`). */
function pickLatestBillingRange(ranges: { from: DateTime; to: DateTime }[]): { from: DateTime; to: DateTime } | null {
  if (ranges.length === 0) return null;
  return ranges.reduce((best, r) => {
    if (r.to > best.to) return r;
    if (r.to < best.to) return best;
    return r.from > best.from ? r : best;
  });
}

export type AggregateYearBilling = {
  gFrom: DateTime | null;
  hTo: DateTime | null;
  /** Invoices overlapping the year window and service start (+1 pending when it overlaps); fallback when DB `no_of_months` absent. */
  billedCycleCount: number;
  L: number;
  /** Sum of paidAmount (Rs) for invoices in window; pending run has no extra paid. */
  totalPaid: number;
};

/**
 * G, H, billed-cycle fallback count, L for one O&M display year (DB invoices + optional pending).
 * **G/H:** latest bill **to** in that display year (pending included for the active year).
 */
export function aggregateInvoicesForYearWindow(
  invoices: OmTenYearInvoiceInput[],
  yearStart: DateTime,
  yearEnd: DateTime,
  omStart: DateTime,
  zone: string,
  options: {
    isCurrentYear: boolean;
    /** When isCurrentYear, this scheduled cycle is not yet in DB. */
    pendingFrom?: DateTime;
    pendingTo?: DateTime;
  }
): AggregateYearBilling {
  const effectiveStart = yearStart >= omStart ? yearStart : omStart;

  const inWindow: OmTenYearInvoiceInput[] = [];
  for (const inv of invoices) {
    const r = effectiveInvoiceRange(inv, zone);
    if (!r) continue;
    if (rangesOverlap(r.from, r.to, yearStart, yearEnd)) inWindow.push(inv);
  }

  let billedCycleCount = 0;
  for (const inv of inWindow) {
    const r = effectiveInvoiceRange(inv, zone);
    if (r && rangesOverlap(r.from, r.to, effectiveStart, yearEnd)) billedCycleCount += 1;
  }

  const billingRanges: { from: DateTime; to: DateTime }[] = [];
  for (const inv of inWindow) {
    const r = effectiveInvoiceRange(inv, zone);
    if (r) billingRanges.push(r);
  }

  if (options.isCurrentYear && options.pendingFrom && options.pendingTo) {
    const pf = options.pendingFrom.startOf('day');
    const pt = options.pendingTo.startOf('day');
    billingRanges.push({ from: pf, to: pt });
    if (rangesOverlap(pf, pt, effectiveStart, yearEnd) && rangesOverlap(pf, pt, yearStart, yearEnd)) {
      billedCycleCount += 1;
    }
  }

  const latest = pickLatestBillingRange(billingRanges);
  const gFrom = latest?.from ?? null;
  const hTo = latest?.to ?? null;

  let L = 0;
  let totalPaid = 0;
  for (const inv of inWindow) {
    const p = inv.penaltyAmount;
    if (p != null && Number.isFinite(Number(p))) L += round2(Number(p));
    const pa = inv.paidAmount;
    if (pa != null && Number.isFinite(Number(pa))) totalPaid += round2(Number(pa));
  }
  L = round2(L);
  totalPaid = round2(totalPaid);

  return { gFrom, hTo, billedCycleCount, L: round2(L), totalPaid };
}

/** Optional DB-backed progress from `scheme_yearly_details` (per display year 1–10). */
export type OmYearlyDbProgress = {
  noOfMonths: number;
  totalBillAmount: number;
  penaltyAmount: number | null;
};

export type ComputeOmTenYearRowsInput = {
  omStartDateIso: string;
  finalExecutedWorkCost: number;
  /** Scheme om_year **1–10** (display year). Used as a fallback when the bill period cannot place the active year. */
  omYear: number;
  invoices: OmTenYearInvoiceInput[];
  /** Current scheduled invoice period (not yet stored). */
  pendingPeriod: { fromIso: string; toIso: string };
  timeZone: string;
  /** When set, billed months (I) and rollups align with `scheme_yearly_details` for that display year. */
  yearlyProgressByYear?: Record<number, OmYearlyDbProgress> | null;
};

export type OmTenYearRowMetric = { year: number; monthsI: number; F: number; J: number };

export type OmTenYearComputeMeta = {
  currentDisplayYear: number;
  inferredDisplayYear: number | null;
  usedDisplayYearFallback: boolean;
  pendingFromIso: string;
  pendingToIso: string;
  omStartDateIso: string;
  rowMetrics: OmTenYearRowMetric[];
};

/**
 * Template columns require **J = I × F** (after rupee rounding). `F` here is the displayed monthly amount.
 */
export function assertOmTenYearJEqualsITimesF(metrics: OmTenYearRowMetric[], tol = 1): void {
  for (const m of metrics) {
    if (m.monthsI <= 0) continue;
    const expected = round2(m.monthsI * m.F);
    if (Math.abs(expected - m.J) > tol) {
      throw new Error(
        `O&M 10-year bill row ${m.year}: J (${m.J}) ≠ I×F (${expected}); I=${m.monthsI}, F=${m.F}`
      );
    }
  }
}

function emptyMeta(omStartDateIso: string, pending: { fromIso: string; toIso: string }): OmTenYearComputeMeta {
  return {
    currentDisplayYear: 1,
    inferredDisplayYear: null,
    usedDisplayYearFallback: true,
    pendingFromIso: pending.fromIso,
    pendingToIso: pending.toIso,
    omStartDateIso,
    rowMetrics: [],
  };
}

/**
 * Build 10 rows (display years 1–10) for O&M 10-year bill template.
 */
export function computeOmTenYearRows(input: ComputeOmTenYearRowsInput): {
  rows: OmTenYearRowView[];
  totals: { omCost: string };
  meta: OmTenYearComputeMeta;
} {
  const { finalExecutedWorkCost, omYear, invoices, pendingPeriod, timeZone: zone, yearlyProgressByYear } = input;
  const cost = finalExecutedWorkCost;
  if (!Number.isFinite(cost) || cost < 0) {
    return { rows: [], totals: { omCost: formatMoney(0) }, meta: emptyMeta(input.omStartDateIso, pendingPeriod) };
  }

  const omStart = parseDateOnly(input.omStartDateIso, zone);
  if (!omStart) {
    return { rows: [], totals: { omCost: formatMoney(0) }, meta: emptyMeta(input.omStartDateIso, pendingPeriod) };
  }

  const pendingFrom = parseDateOnly(pendingPeriod.fromIso, zone);
  const pendingTo = parseDateOnly(pendingPeriod.toIso, zone);
  if (!pendingFrom || !pendingTo) {
    return { rows: [], totals: { omCost: formatMoney(0) }, meta: emptyMeta(input.omStartDateIso, pendingPeriod) };
  }

  const inferredDisplayYear = resolveCurrentOmDisplayYearForPendingBill(
    input.omStartDateIso,
    pendingPeriod.fromIso,
    pendingPeriod.toIso,
    zone
  );
  let currentDisplayYear: number;
  if (
    inferredDisplayYear != null &&
    inferredDisplayYear >= 1 &&
    inferredDisplayYear <= 10
  ) {
    currentDisplayYear = inferredDisplayYear;
  } else {
    currentDisplayYear = Math.min(10, Math.max(1, Math.trunc(omYear)));
  }
  const usedDisplayYearFallback =
    inferredDisplayYear == null || inferredDisplayYear < 1 || inferredDisplayYear > 10;

  const rowMetrics: OmTenYearRowMetric[] = [];

  const rows: OmTenYearRowView[] = [];
  let sumE = 0;
  let runK = 0;

  for (let y = 1; y <= 10; y++) {
    const mult = MULTIPLIER_BY_YEAR[y];
    const yearStart = omStart.plus({ years: y - 1 });
    const yearEnd = yearStart.plus({ years: 1 }).minus({ days: 1 });
    if (!yearStart.isValid || !yearEnd.isValid) continue;

    const E = round2(cost * mult);
    const F = round2(E / 12);
    sumE += E;

    let G = '';
    let H = '';
    let paidM = 0;

    const dbPr = yearlyProgressByYear?.[y];
    const useDbProgress = dbPr != null && y <= currentDisplayYear;
    const pendingOverlapsYear = rangesOverlap(
      pendingFrom.startOf('day'),
      pendingTo.startOf('day'),
      yearStart.startOf('day'),
      yearEnd.startOf('day')
    );

    let ag: AggregateYearBilling = {
      gFrom: null,
      hTo: null,
      billedCycleCount: 0,
      L: 0,
      totalPaid: 0,
    };

    if (y < currentDisplayYear) {
      ag = aggregateInvoicesForYearWindow(invoices, yearStart, yearEnd, omStart, zone, {
        isCurrentYear: false,
      });
      paidM = ag.totalPaid;
      if (ag.gFrom && ag.hTo) {
        G = formatDdMmYyyy(ag.gFrom);
        H = formatDdMmYyyy(ag.hTo);
      } else {
        G = '—';
        H = '—';
      }
    } else if (y === currentDisplayYear) {
      ag = aggregateInvoicesForYearWindow(invoices, yearStart, yearEnd, omStart, zone, {
        isCurrentYear: true,
        pendingFrom,
        pendingTo,
      });
      paidM = ag.totalPaid;
      if (ag.gFrom && ag.hTo) {
        G = formatDdMmYyyy(ag.gFrom);
        H = formatDdMmYyyy(ag.hTo);
      } else {
        G = '—';
        H = '—';
      }
    } else {
      G = '—';
      H = '—';
      paidM = 0;
    }

    let I = 0;
    let Lcol = 0;
    let fin: ReturnType<typeof computeRowFinancials>;

    if (y > currentDisplayYear) {
      fin = computeRowFinancials({ I: 0, F, L: 0 });
    } else if (useDbProgress && dbPr) {
      I = Math.min(12, Math.max(0, Math.trunc(Number(dbPr.noOfMonths) || 0)));
      if (I === 0 && y === currentDisplayYear && pendingOverlapsYear) {
        I = 1;
      }
      if (dbPr.penaltyAmount != null && Number.isFinite(Number(dbPr.penaltyAmount))) {
        Lcol = round2(Number(dbPr.penaltyAmount));
      } else {
        Lcol = ag.L;
      }
      const dbJ = round2(Number(dbPr.totalBillAmount) || 0);
      const Fused = I > 0 && dbJ > 0 ? dbJ / I : F;
      fin = computeRowFinancials({ I, F: Fused, L: Lcol });
    } else {
      I = ag.billedCycleCount;
      Lcol = ag.L;
      fin = computeRowFinancials({ I, F, L: Lcol });
    }

    const Fdisplay = I > 0 ? round2(fin.J / I) : F;
    if (y <= currentDisplayYear) {
      rowMetrics.push({ year: y, monthsI: I, F: Fdisplay, J: fin.J });
    }

    runK = round2(runK + fin.J);

    const monthsDisplay = y > currentDisplayYear ? '—' : String(Math.trunc(I));

    const billPaidDisplay =
      y > currentDisplayYear ? '—' : paidM > 0 ? formatMoney(paidM) : '—';

    rows.push({
      year: y,
      yearLabel: formatOmDisplayYearLabel(y),
      start: formatDdMmYyyy(yearStart),
      end: formatDdMmYyyy(yearEnd),
      percent: formatPercentFromMultiplier(mult),
      amount: formatMoney(E),
      monthly: y > currentDisplayYear ? '—' : formatMoney(Fdisplay),
      from: G,
      to: H,
      months: monthsDisplay,
      totalBill: y > currentDisplayYear ? '—' : formatMoney(fin.J),
      cumulative: y > currentDisplayYear ? '—' : formatMoney(runK),
      penalty: y > currentDisplayYear ? '—' : formatMoney(Lcol),
      billPaid: y > currentDisplayYear ? '—' : billPaidDisplay,
      finalBasic: y > currentDisplayYear ? '—' : formatMoney(fin.N),
      gst18: y > currentDisplayYear ? '—' : formatMoney(fin.O),
      totalPayable: y > currentDisplayYear ? '—' : formatMoney(fin.P),
      gstDed2: y > currentDisplayYear ? '—' : formatMoney(fin.Q),
      tdsDed2: y > currentDisplayYear ? '—' : formatMoney(fin.R),
      labourCess: y > currentDisplayYear ? '—' : formatMoney(fin.S),
      totalDeduction: y > currentDisplayYear ? '—' : formatMoney(fin.T),
      finalAmount: y > currentDisplayYear ? '—' : formatMoney(fin.U),
    });
  }

  return {
    rows,
    totals: { omCost: formatMoney(sumE) },
    meta: {
      currentDisplayYear,
      inferredDisplayYear,
      usedDisplayYearFallback,
      pendingFromIso: pendingPeriod.fromIso,
      pendingToIso: pendingPeriod.toIso,
      omStartDateIso: input.omStartDateIso,
      rowMetrics,
    },
  };
}
