/**
 * Form 16 (Form 16A TDS Credit) service.
 * Quarter-based reconciliation: 26AS (aggregated by tan+fy+quarter), Form 16A match, credit/debit, ledger.
 *
 * Credit note: run26asMatchAndCreditNote only (on Form 16A submit, match 26AS → CN-F-16-{...}, ledger, CSV to WFM FORM_16).
 * Debit: process26asUploadAggregation only (when 26AS total drops for a SETTLED quarter); DN-F-16-{...}, CSV to WFM FORM_16.
 */

import crypto from 'crypto';
import { Op, fn, col, QueryTypes, where as sqlWhere } from 'sequelize';
import { sequelize } from '../config/database';
import {
  Form16CreditNote,
  Form16DebitNote,
  Form16aSubmission,
  WorkflowRequest,
  Document,
  Form1626asQuarterSnapshot,
  Form16QuarterStatus,
  Form16LedgerEntry,
  Form16SapResponse,
} from '../models';
import { Tds26asEntry } from '../models/Tds26asEntry';
import { Form1626asUploadLog } from '../models/Form1626asUploadLog';
import { Form16NonSubmittedNotification } from '../models/Form16NonSubmittedNotification';
import { Dealer } from '../models/Dealer';
import { User } from '../models/User';
import { Priority, WorkflowStatus } from '../types/common.types';
import { generateRequestNumber, padDealerCode } from '../utils/helpers';
import { gcsStorageService } from './gcsStorage.service';
import { activityService } from './activity.service';
import { wfmFileService } from './wfmFile.service';
import logger from '../utils/logger';

/**
 * Resolve dealer_code for the current user.
 * Uses users.employee_number (DB column) first — dealer code saved at login; else falls back to email match with dealers.dealer_principal_email_id.
 * Same dealer code is used for submission, credit note, and debit note generation.
 * Returns null if no dealer code found.
 */
export async function getDealerCodeForUser(userId: string, userEmail?: string | null): Promise<string | null> {
  const [row] = await sequelize.query<{ employee_number: string | null }>(
    `SELECT employee_number FROM users WHERE user_id = :userId LIMIT 1`,
    { replacements: { userId }, type: QueryTypes.SELECT }
  );
  if (row?.employee_number != null && String(row.employee_number).trim()) {
    return String(row.employee_number).trim();
  }

  const user = await User.findByPk(userId, { attributes: ['userId', 'email'] });
  const emailToUse = (user?.email || userEmail || '').toString().trim();
  if (!emailToUse) return null;
  const dealer = await Dealer.findOne({
    where: {
      dealerPrincipalEmailId: { [Op.iLike]: emailToUse },
      isActive: true,
    },
    attributes: ['salesCode', 'dlrcode', 'dealerId'],
  });

  if (!dealer) return null;
  const code = dealer.salesCode || dealer.dlrcode || null;
  return code;
}

/** 26AS: only Section 194Q and Booking Status F or O are considered for aggregation and matching. */
const SECTION_26AS_194Q = '194Q';
const AMOUNT_MATCH_TOLERANCE = 1;

/**
 * Form 16 INCOMING CSV `TDS_AMT`: amount digits first, sign last — credit `123.45+`, debit `123.45-`.
 */
function formatForm16IncomingCsvTdsAmt(amount: number, kind: 'credit' | 'debit'): string {
  const n = Math.abs(Number(amount)).toFixed(2);
  return kind === 'credit' ? `${n}+` : `${n}-`;
}

type Latest26asRow = {
  panNumber: string | null;
  amountPaid: number | null;
  taxDeducted: number;
  totalTdsDeposited: number | null;
  transactionDate: string | null;
  dateOfBooking: string | null;
};

function normalizeTanNumber(raw: unknown): string {
  return String(raw ?? '')
    .trim()
    .toUpperCase()
    .replace(/[^A-Z0-9]/g, '');
}

// Some environments might still be on the old DB schema.
// If `tds_26as_entries.pan_number` is missing, uploads/selects will fail without this guard.
let _panNumberColumnPresent: boolean | null = null;
async function isPanNumberColumnPresent(): Promise<boolean> {
  if (_panNumberColumnPresent !== null) return _panNumberColumnPresent;
  try {
    const [row] = await sequelize.query<{ exists: boolean }>(
      `SELECT CASE WHEN COUNT(*) > 0 THEN true ELSE false END AS exists
       FROM information_schema.columns
       WHERE table_name = 'tds_26as_entries' AND column_name = 'pan_number'`,
      { type: QueryTypes.SELECT }
    );
    _panNumberColumnPresent = !!row?.exists;
  } catch (e) {
    logger.warn('[Form16] pan_number column presence check failed; disabling PAN persistence/enforcement.', {
      error: e instanceof Error ? e.message : String(e),
    });
    _panNumberColumnPresent = false;
  }
  return _panNumberColumnPresent;
}

/**
 * Get aggregated TDS amount for (tan, fy, quarter) from the LATEST 26AS upload only (Section 194Q, Booking F/O).
 * Use case: "Always match Form 16A only with the latest 26AS version." Each upload can be full cumulative;
 * we sum only rows from the most recent upload for that quarter to avoid double-counting across uploads.
 * If no rows have upload_log_id (legacy), falls back to summing all rows for that quarter.
 */
export async function getLatest26asAggregatedForQuarter(
  tanNumber: string,
  financialYear: string,
  quarter: string
): Promise<number> {
  const normalizedTan = normalizeTanNumber(tanNumber);
  const fy = normalizeFinancialYear(financialYear) || financialYear;
  const q = normalizeQuarter(quarter) || quarter;
  const [row] = await sequelize.query<{ sum: string }>(
    `WITH latest_upload AS (
       SELECT MAX(upload_log_id) AS mid FROM tds_26as_entries
       WHERE UPPER(REGEXP_REPLACE(TRIM(COALESCE(tan_number, '')), '[^a-zA-Z0-9]', '', 'g')) = :tan
         AND financial_year = :fy AND quarter = :qtr
         AND UPPER(TRIM(COALESCE(section_code, ''))) = :section
         AND UPPER(TRIM(COALESCE(status_oltas, ''))) IN ('F', 'O')
         AND upload_log_id IS NOT NULL
     )
     SELECT COALESCE(SUM(e.tax_deducted), 0)::text AS sum
     FROM tds_26as_entries e
     WHERE UPPER(REGEXP_REPLACE(TRIM(COALESCE(e.tan_number, '')), '[^a-zA-Z0-9]', '', 'g')) = :tan
       AND e.financial_year = :fy AND e.quarter = :qtr
       AND UPPER(TRIM(COALESCE(e.section_code, ''))) = :section
       AND UPPER(TRIM(COALESCE(e.status_oltas, ''))) IN ('F', 'O')
       AND (
         e.upload_log_id = (SELECT mid FROM latest_upload)
         OR (SELECT mid FROM latest_upload) IS NULL
       )`,
    { replacements: { tan: normalizedTan, fy, qtr: q, section: SECTION_26AS_194Q }, type: QueryTypes.SELECT }
  );
  return parseFloat(row?.sum ?? '0') || 0;
}

async function getLatest26asRowsForQuarter(
  tanNumber: string,
  financialYear: string,
  quarter: string
): Promise<Latest26asRow[]> {
  const normalizedTan = normalizeTanNumber(tanNumber);
  const fy = normalizeFinancialYear(financialYear) || financialYear;
  const q = normalizeQuarter(quarter) || quarter;

  const hasPan = await isPanNumberColumnPresent();
  const panSelect = hasPan ? 'e.pan_number' : 'NULL::text AS pan_number';

  const rows = await sequelize.query<{
    pan_number: string | null;
    amount_paid: string | null;
    tax_deducted: string;
    total_tds_deposited: string | null;
    transaction_date: string | null;
    date_of_booking: string | null;
  }>(
    `WITH latest_upload AS (
       SELECT MAX(upload_log_id) AS mid FROM tds_26as_entries
       WHERE UPPER(REGEXP_REPLACE(TRIM(COALESCE(tan_number, '')), '[^a-zA-Z0-9]', '', 'g')) = :tan
         AND financial_year = :fy AND quarter = :qtr
         AND UPPER(TRIM(COALESCE(section_code, ''))) = :section
         AND UPPER(TRIM(COALESCE(status_oltas, ''))) IN ('F', 'O')
         AND upload_log_id IS NOT NULL
     )
     SELECT
       ${panSelect},
       e.amount_paid,
       e.tax_deducted,
       e.total_tds_deposited,
       e.transaction_date,
       e.date_of_booking
     FROM tds_26as_entries e
     WHERE UPPER(REGEXP_REPLACE(TRIM(COALESCE(e.tan_number, '')), '[^a-zA-Z0-9]', '', 'g')) = :tan
       AND e.financial_year = :fy
       AND e.quarter = :qtr
       AND UPPER(TRIM(COALESCE(e.section_code, ''))) = :section
       AND UPPER(TRIM(COALESCE(e.status_oltas, ''))) IN ('F', 'O')
       AND (
         e.upload_log_id = (SELECT mid FROM latest_upload)
         OR (SELECT mid FROM latest_upload) IS NULL
       )`,
    { replacements: { tan: normalizedTan, fy, qtr: q, section: SECTION_26AS_194Q }, type: QueryTypes.SELECT }
  );

  return rows.map((r) => ({
    panNumber: r.pan_number ? String(r.pan_number).trim().toUpperCase() : null,
    amountPaid: r.amount_paid == null ? null : parseFloat(r.amount_paid),
    taxDeducted: parseFloat(r.tax_deducted || '0') || 0,
    totalTdsDeposited: r.total_tds_deposited == null ? null : parseFloat(r.total_tds_deposited),
    transactionDate: r.transaction_date || null,
    dateOfBooking: r.date_of_booking || null,
  }));
}

async function get26asCoverageDebug(tanNumber: string, financialYear: string, quarter: string) {
  const normalizedTan = normalizeTanNumber(tanNumber);
  const fy = normalizeFinancialYear(financialYear) || financialYear;
  const q = normalizeQuarter(quarter) || quarter;

  // Overall counts + how many rows qualify for our matching rule:
  const [counts] = await sequelize.query<{
    total_rows: string;
    matching_194q_f_o_rows: string;
  }>(
    `SELECT
      COUNT(*)::text AS total_rows,
      SUM(
        CASE
          WHEN UPPER(TRIM(COALESCE(section_code, ''))) = :section
           AND UPPER(TRIM(COALESCE(status_oltas, ''))) IN ('F', 'O')
          THEN 1 ELSE 0
        END
      )::text AS matching_194q_f_o_rows
     FROM tds_26as_entries e
    WHERE UPPER(REGEXP_REPLACE(TRIM(COALESCE(e.tan_number, '')), '[^a-zA-Z0-9]', '', 'g')) = :tan
       AND e.financial_year = :fy
       AND e.quarter = :q`,
    { replacements: { tan: normalizedTan, fy, q, section: SECTION_26AS_194Q }, type: QueryTypes.SELECT }
  );

  // Section/status breakdown to make it obvious why latestRows became empty.
  const breakdown = (await sequelize.query(
    `SELECT
      section_code,
      status_oltas,
      COUNT(*)::text AS cnt
     FROM tds_26as_entries e
    WHERE UPPER(REGEXP_REPLACE(TRIM(COALESCE(e.tan_number, '')), '[^a-zA-Z0-9]', '', 'g')) = :tan
       AND e.financial_year = :fy
       AND e.quarter = :q
     GROUP BY section_code, status_oltas
     ORDER BY cnt DESC
     LIMIT 8`,
    { replacements: { tan: normalizedTan, fy, q }, type: QueryTypes.SELECT }
  )) as Array<{ section_code: string | null; status_oltas: string | null; cnt: string }>;

  const totalRows = parseInt(String(counts?.total_rows ?? '0'), 10) || 0;
  const matchingRows = parseInt(String(counts?.matching_194q_f_o_rows ?? '0'), 10) || 0;

  const breakdownLines = (breakdown || [])
    .map((b) => {
      const sec = (b.section_code ?? '').toString().trim() || '(null)';
      const st = (b.status_oltas ?? '').toString().trim() || '(null)';
      const c = parseInt(String(b.cnt ?? '0'), 10) || 0;
      return `${sec}/${st}:${c}`;
    })
    .join(', ');

  return { totalRows, matchingRows, breakdownLines };
}

function normalizeDateOnly(value: unknown): string | null {
  if (!value) return null;
  const raw = String(value).trim();
  if (!raw) return null;

  // Prefer Indian-style numeric dates from OCR (DD-MM-YYYY or DD/MM/YYYY).
  // Do NOT let JS Date parse these first, because it may interpret as MM-DD-YYYY.
  const m = raw.match(/^(\d{1,2})[-\/](\d{1,2})[-\/](\d{2,4})$/);
  if (m) {
    const dd = m[1].padStart(2, '0');
    const mm = m[2].padStart(2, '0');
    const yyyy = m[3].length === 2 ? `20${m[3]}` : m[3];
    return `${yyyy}-${mm}-${dd}`;
  }

  // Handle OCR values like "13-Jan-2025" without timezone conversion.
  const m2 = raw.match(/^(\d{1,2})[-\/]([A-Za-z]{3,9})[-\/](\d{4})$/);
  if (m2) {
    const dd = m2[1].padStart(2, '0');
    const mon = m2[2].toLowerCase();
    const yyyy = m2[3];
    const monthMap: Record<string, string> = {
      jan: '01', january: '01',
      feb: '02', february: '02',
      mar: '03', march: '03',
      apr: '04', april: '04',
      may: '05',
      jun: '06', june: '06',
      jul: '07', july: '07',
      aug: '08', august: '08',
      sep: '09', sept: '09', september: '09',
      oct: '10', october: '10',
      nov: '11', november: '11',
      dec: '12', december: '12',
    };
    const mm = monthMap[mon];
    if (mm) return `${yyyy}-${mm}-${dd}`;
  }

  const d = new Date(raw);
  if (!Number.isNaN(d.getTime())) return d.toISOString().slice(0, 10);
  return null;
}

/**
 * Derive Indian FY and 26AS quarter from an OCR date-only string (YYYY-MM-DD).
 * Uses the same quarter boundaries as 26AS dateToFyAndQuarter:
 * - Apr-Jun => Q1 (FY end = year+1)
 * - Jul-Sep => Q2
 * - Oct-Dec => Q3
 * - Jan-Mar => Q4 (FY end = year)
 */
function deriveFyAndQuarterFromDateOnly(dateOnly: string | null): { financialYear: string; quarter: string } | null {
  if (!dateOnly) return null;
  const d = new Date(`${dateOnly}T00:00:00.000Z`);
  if (Number.isNaN(d.getTime())) return null;

  const month = d.getUTCMonth() + 1; // 1-12
  const year = d.getUTCFullYear();

  let quarter: string;
  if ([4, 5, 6].includes(month)) quarter = 'Q1';
  else if ([7, 8, 9].includes(month)) quarter = 'Q2';
  else if ([10, 11, 12].includes(month)) quarter = 'Q3';
  else quarter = 'Q4'; // Jan-Mar

  const fyEnd = [1, 2, 3].includes(month) ? year : year + 1;
  const fyStart = fyEnd - 1;
  const next = (fyEnd % 100).toString().padStart(2, '0');
  return { financialYear: `${fyStart}-${next}`, quarter };
}

function toNumberOrNull(value: unknown): number | null {
  if (value == null || value === '') return null;
  const n = typeof value === 'number' ? value : parseFloat(String(value).replace(/,/g, ''));
  return Number.isFinite(n) ? n : null;
}

/** Get latest 26AS quarter snapshot for (tan, fy, quarter). */
export async function getLatest26asSnapshot(
  tanNumber: string,
  financialYear: string,
  quarter: string
): Promise<Form1626asQuarterSnapshot | null> {
  const normalized = (tanNumber || '').trim().replace(/\s+/g, ' ');
  const fy = normalizeFinancialYear(financialYear) || financialYear;
  const q = normalizeQuarter(quarter) || quarter;
  return Form1626asQuarterSnapshot.findOne({
    where: {
      tanNumber: { [Op.iLike]: normalized },
      financialYear: fy,
      quarter: q,
    },
    order: [['createdAt', 'DESC']],
  });
}

/** Get quarter status row for (tan, fy, quarter). */
export async function getQuarterStatus(
  tanNumber: string,
  financialYear: string,
  quarter: string
): Promise<Form16QuarterStatus | null> {
  const fy = normalizeFinancialYear(financialYear) || financialYear;
  const q = normalizeQuarter(quarter) || quarter;
  const normalized = (tanNumber || '').trim().replace(/\s+/g, ' ');
  return Form16QuarterStatus.findOne({
    where: {
      tanNumber: { [Op.iLike]: normalized },
      financialYear: fy,
      quarter: q,
    },
  });
}

/** Create or update quarter status to SETTLED with given credit note. */
async function setQuarterStatusSettled(
  tanNumber: string,
  financialYear: string,
  quarter: string,
  creditNoteId: number
): Promise<void> {
  const fy = normalizeFinancialYear(financialYear) || financialYear;
  const q = normalizeQuarter(quarter) || quarter;
  const normalized = (tanNumber || '').trim().replace(/\s+/g, ' ');
  const [status] = await Form16QuarterStatus.findOrCreate({
    where: { tanNumber: normalized, financialYear: fy, quarter: q },
    defaults: {
      tanNumber: normalized,
      financialYear: fy,
      quarter: q,
      status: 'SETTLED',
      lastCreditNoteId: creditNoteId,
      lastDebitNoteId: null,
      updatedAt: new Date(),
    },
  });
  await status.update({
    status: 'SETTLED',
    lastCreditNoteId: creditNoteId,
    lastDebitNoteId: null,
    updatedAt: new Date(),
  });
}

/** Create or update quarter status to DEBIT_ISSUED_PENDING_FORM16 with given debit note. */
async function setQuarterStatusDebitIssued(
  tanNumber: string,
  financialYear: string,
  quarter: string,
  debitNoteId: number
): Promise<void> {
  const fy = normalizeFinancialYear(financialYear) || financialYear;
  const q = normalizeQuarter(quarter) || quarter;
  const normalized = (tanNumber || '').trim().replace(/\s+/g, ' ');
  const [status] = await Form16QuarterStatus.findOrCreate({
    where: { tanNumber: normalized, financialYear: fy, quarter: q },
    defaults: {
      tanNumber: normalized,
      financialYear: fy,
      quarter: q,
      status: 'DEBIT_ISSUED_PENDING_FORM16',
      lastDebitNoteId: debitNoteId,
      lastCreditNoteId: null,
      updatedAt: new Date(),
    },
  });
  await status.update({
    status: 'DEBIT_ISSUED_PENDING_FORM16',
    lastDebitNoteId: debitNoteId,
    updatedAt: new Date(),
  });
}

/** Add a ledger entry (CREDIT or DEBIT). No deletion; full audit trail. */
async function addLedgerEntry(params: {
  tanNumber: string;
  financialYear: string;
  quarter: string;
  entryType: 'CREDIT' | 'DEBIT';
  amount: number;
  creditNoteId?: number | null;
  debitNoteId?: number | null;
  form16SubmissionId?: number | null;
  snapshotId?: number | null;
}): Promise<void> {
  const fy = normalizeFinancialYear(params.financialYear) || params.financialYear;
  const q = normalizeQuarter(params.quarter) || params.quarter;
  const normalized = (params.tanNumber || '').trim().replace(/\s+/g, ' ');
  await Form16LedgerEntry.create({
    tanNumber: normalized,
    financialYear: fy,
    quarter: q,
    entryType: params.entryType,
    amount: params.amount,
    creditNoteId: params.creditNoteId ?? null,
    debitNoteId: params.debitNoteId ?? null,
    form16SubmissionId: params.form16SubmissionId ?? null,
    snapshotId: params.snapshotId ?? null,
    createdAt: new Date(),
  });
}

/**
 * List credit notes for the dealer associated with the current user.
 * Used by dealer-facing Credit Notes page under Form 16.
 */
export async function listCreditNotesForDealer(userId: string, filters?: { financialYear?: string; quarter?: string }) {
  const dealerCode = await getDealerCodeForUser(userId);
  if (!dealerCode) {
    return { rows: [], total: 0 };
  }

  // If DB migrations for TRNS uniq no are not applied yet, do not break listing.
  // We'll return sapResponseAvailable=false in that case.
  let hasTrnsUniqNoColumn = true;
  try {
    await sequelize.query(`SELECT trns_uniq_no FROM form_16_credit_notes LIMIT 1`, { type: QueryTypes.SELECT });
  } catch {
    hasTrnsUniqNoColumn = false;
  }

  const whereSubmission: any = { dealerCode };
  if (filters?.financialYear) whereSubmission.financialYear = filters.financialYear;
  if (filters?.quarter) whereSubmission.quarter = filters.quarter;

  const submissions = await Form16aSubmission.findAll({
    where: whereSubmission,
    attributes: ['id', 'requestId', 'form16aNumber', 'financialYear', 'quarter', 'status', 'submittedDate'],
  });
  const submissionIds = submissions.map((s) => s.id);
  if (submissionIds.length === 0) {
    return { rows: [], total: 0 };
  }

  const { rows, count } = await Form16CreditNote.findAndCountAll({
    where: { submissionId: { [Op.in]: submissionIds } },
    include: [
      {
        model: Form16aSubmission,
        as: 'submission',
        attributes: ['id', 'requestId', 'form16aNumber', 'financialYear', 'quarter', 'status'],
      },
    ],
    order: [['issueDate', 'DESC'], ['createdAt', 'DESC']],
  });

  const noteIds = rows.map((r) => r.id);
  let sapSet = new Set<number>();
  if (hasTrnsUniqNoColumn && noteIds.length) {
    try {
      const creditNotes = await Form16CreditNote.findAll({
        where: { id: { [Op.in]: noteIds } },
        attributes: ['id', 'creditNoteNumber'],
        raw: true,
      }) as any[];
      const creditNumbers = creditNotes.map((n) => n.creditNoteNumber).filter(Boolean);
      const sapRows = await (Form16SapResponse as any).findAll({
        where: { tdsTransId: { [Op.in]: creditNumbers } },
        attributes: ['tdsTransId'],
        raw: true,
      });
      const available = new Set((sapRows as any[]).map((r) => String(r.tdsTransId)));
      sapSet = new Set(creditNotes.filter((n) => available.has(String(n.creditNoteNumber))).map((n) => Number(n.id)));
    } catch (e: any) {
      logger.warn('[Form16] SAP response lookup failed (will treat as unavailable):', e?.message || e);
      sapSet = new Set<number>();
    }
  }

  const dealer = await Dealer.findOne({
    where: { [Op.or]: [{ salesCode: dealerCode }, { dlrcode: dealerCode }], isActive: true },
    attributes: ['dealership', 'dealerPrincipalName'],
  });
  const dealerName = dealer ? ((dealer as any).dealership || (dealer as any).dealerPrincipalName || dealerCode) : dealerCode;

  return {
    rows: rows.map((r) => ({
      id: r.id,
      creditNoteNumber: r.creditNoteNumber,
      sapDocumentNumber: r.sapDocumentNumber,
      sapResponseAvailable: sapSet.has(r.id),
      amount: r.amount,
      issueDate: r.issueDate,
      financialYear: r.financialYear,
      quarter: r.quarter,
      status: r.status,
      remarks: r.remarks,
      dealerCode,
      dealerName,
      submission: r.submission
        ? {
            requestId: r.submission.requestId,
            form16aNumber: r.submission.form16aNumber,
            financialYear: r.submission.financialYear,
            quarter: r.submission.quarter,
            status: r.submission.status,
          }
        : null,
    })),
    total: count,
    summary: {
      totalCreditNotes: count,
      totalAmount: rows.reduce((sum, r) => sum + (Number(r.amount) || 0), 0),
      activeDealersCount: 1,
    },
  };
}

export interface CreateForm16SubmissionBody {
  /**
   * Optional override for RE/UAT users who are not mapped as a dealer by email.
   * If user is a dealer, this (when provided) must match the resolved dealerCode.
   */
  dealerCode?: string;
  financialYear: string;
  quarter: string;
  form16aNumber: string;
  tdsAmount: number;
  totalAmount: number;
  tanNumber: string;
  deductorName: string;
  version?: number;
  /** Raw OCR extracted JSON for audit/support (optional). */
  ocrExtractedData?: Record<string, unknown> | null;
}

export interface CreateForm16SubmissionResult {
  requestId: string;
  requestNumber: string;
  submissionId: number;
  /** Set when 26AS matching runs synchronously: 'success' | 'failed' | 'resubmission_needed' | 'duplicate' */
  validationStatus?: string;
  /** Credit note number when validationStatus === 'success' */
  creditNoteNumber?: string | null;
  /** Message for dealer when validation failed / resubmission needed / duplicate */
  validationNotes?: string;
}

/**
 * Get next version number for dealer + FY + quarter (for versioning Form 16 uploads per FY+quarter).
 */
async function getNextVersionForDealerFyQuarter(dealerCode: string, financialYear: string, quarter: string): Promise<number> {
  const rows = await Form16aSubmission.findAll({
    where: { dealerCode, financialYear, quarter },
    attributes: ['version'],
    order: [['version', 'DESC']],
    limit: 1,
  });
  const maxVersion = rows.length > 0 ? Math.max(0, ...(rows as any[]).map((r) => (r.version ?? 1))) : 0;
  return maxVersion + 1;
}

/**
 * Check if there is already an active (non-withdrawn) credit note for this dealer + FY + quarter.
 * Optional fyAlternates and quarterAlternates allow matching submissions stored with raw/alternate formats.
 */
async function hasActiveCreditNoteForDealerFyQuarter(
  dealerCode: string,
  financialYear: string,
  quarter: string,
  alternates?: { fyAlternates?: string[]; quarterAlternates?: string[] }
): Promise<boolean> {
  const fySet = [...new Set([financialYear, ...(alternates?.fyAlternates || [])].filter(Boolean))];
  const qSet = [...new Set([quarter, ...(alternates?.quarterAlternates || [])].filter(Boolean))];
  const submissions = await Form16aSubmission.findAll({
    where: {
      dealerCode,
      financialYear: fySet.length ? { [Op.in]: fySet } : financialYear,
      quarter: qSet.length ? { [Op.in]: qSet } : quarter,
    },
    attributes: ['id'],
  });
  if (submissions.length === 0) return false;
  const submissionIds = submissions.map((s) => s.id);
  const activeNote = await Form16CreditNote.findOne({
    where: {
      submissionId: { [Op.in]: submissionIds },
      status: { [Op.ne]: 'withdrawn' },
    },
    attributes: ['id'],
  });
  return !!activeNote;
}

/**
 * Normalize financial year to 26AS format (e.g. "2024-25").
 * Handles: "2024-25", "24-25", "FY 2024-25", "FY2024-25".
 */
function normalizeFinancialYear(raw: string): string {
  const s = (raw || '').trim().replace(/^FY\s*/i, '');
  if (!s) return '';
  const m = s.match(/^(\d{2,4})-(\d{2})$/);
  if (m) {
    const start = m[1].length === 2 ? 2000 + parseInt(m[1], 10) : parseInt(m[1], 10);
    return `${start}-${m[2]}`;
  }
  return s;
}

/**
 * Normalize quarter to 26AS format (Q1, Q2, Q3, Q4).
 * Handles: "Q1", "1", "Quarter 1", "Q 1".
 */
function normalizeQuarter(raw: string): string {
  const s = (raw || '').trim().toUpperCase().replace(/\s+/g, '');
  if (/^Q?[1-4]$/.test(s)) return s.length === 1 ? `Q${s}` : s;
  const m = s.match(/QUARTER?([1-4])/i);
  if (m) return `Q${m[1]}`;
  return (raw || '').trim() || '';
}

/**
 * Assessment Year from Financial Year (Indian income tax): FY 2024-25 → AY 2025-26.
 */
function financialYearToAssessmentYear(financialYear: string): string {
  const fy = normalizeFinancialYear(financialYear) || (financialYear || '').trim();
  const m = /^(\d{4})-(\d{2})$/.exec(fy);
  if (!m) return fy.replace(/[^\w.-]/g, '_').slice(0, 24) || 'AY';
  const y1 = parseInt(m[1], 10);
  const ayStart = y1 + 1;
  const ayEnd2 = (y1 + 2) % 100;
  return `${ayStart}-${String(ayEnd2).padStart(2, '0')}`;
}

function sanitizeForm16PdfDeductorSegment(text: string, maxLen: number): string {
  let s = String(text || '')
    .replace(/[\r\n]+/g, ' ')
    .replace(/[<>:"/\\|?*\x00-\x1f]/g, '')
    .replace(/\s+/g, ' ')
    .trim();
  if (!s) return 'Deductor';
  if (s.length > maxLen) s = s.slice(0, maxLen).trim();
  return s;
}

function sanitizeForm16PdfCertSegment(text: string): string {
  const t = String(text || '').trim().replace(/[^A-Za-z0-9_-]/g, '');
  return t || 'CERT';
}

/**
 * PDF file name after successful 26AS match + credit note:
 * [TAN]_[Assessment Year]_[Quarter]_[Name and address of deductor]_[Certificate].pdf
 */
function buildForm16CreditNoteSuccessPdfFileName(sub: Form16aSubmission): string {
  const tan = normalizeTanNumber(String(sub.tanNumber || ''))
    .replace(/[^A-Z0-9]/gi, '')
    .toUpperCase() || 'TAN';
  const fy = normalizeFinancialYear(String(sub.financialYear || '').trim()) || String(sub.financialYear || '').trim();
  const ay = financialYearToAssessmentYear(fy);
  const qRaw = String(sub.quarter || '').trim();
  const q = normalizeQuarter(qRaw) || qRaw || 'QX';
  const ocr = (sub.ocrExtractedData || {}) as Record<string, unknown>;
  let nameAddr = String(ocr.nameAndAddressOfDeductor || '').trim();
  if (!nameAddr) {
    const dn = String(ocr.deductorName || sub.deductorName || '').trim();
    const da = String(ocr.deductorAddress || '').trim();
    nameAddr = [dn, da].filter(Boolean).join(', ');
  }
  if (!nameAddr) nameAddr = String(sub.deductorName || 'Deductor').trim();
  let deductorSan = sanitizeForm16PdfDeductorSegment(nameAddr, 150);
  const certSan = sanitizeForm16PdfCertSegment(String(sub.form16aNumber || ''));
  let base = `${tan}_${ay}_${q}_${deductorSan}_${certSan}`;
  if (base.length > 220) {
    const over = base.length - 220;
    const shorter = Math.max(20, deductorSan.length - over - 5);
    deductorSan = sanitizeForm16PdfDeductorSegment(nameAddr, shorter);
    base = `${tan}_${ay}_${q}_${deductorSan}_${certSan}`;
  }
  return `${base}.pdf`;
}

async function renameForm16SubmissionPdfAfterCreditNote(params: {
  submissionId: number;
  requestId: string;
  oldRelativePath: string;
}): Promise<void> {
  const { submissionId, requestId, oldRelativePath } = params;
  logger.info('[Form16] PDF rename flow start', {
    submissionId,
    requestId,
    oldRelativePath,
    nodeEnv: process.env.NODE_ENV,
    gcpBucket: process.env.GCP_BUCKET_NAME || null,
  });
  const oldPathNorm = String(oldRelativePath || '').replace(/\\/g, '/').trim();
  if (!oldPathNorm || oldPathNorm.includes('..') || !oldPathNorm.startsWith('requests/')) {
    logger.warn('[Form16] Skip PDF rename: invalid storage path', { oldPathNorm });
    return;
  }

  const sub = await Form16aSubmission.findByPk(submissionId);
  if (!sub) {
    logger.warn('[Form16] Skip PDF rename: submission not found', { submissionId, requestId });
    return;
  }

  const newFileName = buildForm16CreditNoteSuccessPdfFileName(sub);
  logger.info('[Form16] PDF rename target name computed', {
    submissionId,
    requestId,
    newFileName,
    tanNumber: (sub as any).tanNumber || null,
    financialYear: (sub as any).financialYear || null,
    quarter: (sub as any).quarter || null,
    form16aNumber: (sub as any).form16aNumber || null,
  });

  try {
    const result = await gcsStorageService.renameRequestDocumentFile({
      oldRelativePath: oldPathNorm,
      newFileName,
    });
    logger.info('[Form16] Storage rename success', {
      submissionId,
      requestId,
      oldPathNorm,
      renamedFilePath: result.filePath,
      renamedStorageUrlPrefix: String(result.storageUrl || '').slice(0, 120),
    });
    await sub.update({ documentUrl: result.storageUrl });
    logger.info('[Form16] Submission documentUrl updated after rename', {
      submissionId,
      requestId,
    });

    const doc = await Document.findOne({
      where: { requestId, filePath: oldPathNorm },
    });
    if (doc) {
      const fp = result.filePath.length <= 500 ? result.filePath : result.filePath.slice(0, 500);
      const su =
        result.storageUrl.length <= 500 ? result.storageUrl : undefined;
      await doc.update({
        fileName: newFileName.slice(0, 255),
        originalFileName: newFileName.slice(0, 255),
        filePath: fp,
        storageUrl: su,
      });
      logger.info('[Form16] Document metadata updated after rename', {
        requestId,
        submissionId,
        documentId: (doc as any).id || null,
        oldPathNorm,
        newPath: fp,
      });
    } else {
      logger.warn('[Form16] PDF renamed; documents row not found for path', { requestId, oldPathNorm });
    }
    logger.info('[Form16] Form 16A PDF renamed after credit note', { submissionId, newFileName });
  } catch (e: any) {
    logger.error('[Form16] Failed to rename Form 16 PDF after credit note:', e?.message || e);
  }
}

/** Compact FY for Form 16 note numbers: "2024-25" -> "24-25" */
function form16FyCompact(financialYear: string): string {
  const fy = normalizeFinancialYear(financialYear) || (financialYear || '').trim();
  if (!fy) return '';
  const m = fy.match(/^(\d{2,4})-(\d{2})$/);
  if (m) {
    const start = m[1].length === 2 ? m[1] : m[1].slice(-2);
    return `${start}-${m[2]}`;
  }
  return fy;
}

/** FY Short for 16-char ID: "2025-26" -> "26" */
function form16FyShort(financialYear: string): string {
  const fy = normalizeFinancialYear(financialYear) || (financialYear || '').trim();
  const m = fy.match(/-(\d{2})$/);
  return m ? m[1] : (fy.length >= 2 ? fy.slice(-2) : 'XX');
}

/** Get next sequence for Form 16 credit note (4 digits) */
async function getNextForm16NoteSequence(
  dealerCode: string,
  financialYear: string,
  quarter: string
): Promise<string> {
  const dc = padDealerCode(dealerCode);
  const fy = form16FyShort(financialYear);
  const q = normalizeQuarter(quarter);
  const prefix = `CN${dc}${fy}${q}`;

  const lastNote = (await Form16CreditNote.findOne({
    where: {
      creditNoteNumber: { [Op.like]: `${prefix}%` },
    },
    order: [['creditNoteNumber', 'DESC']],
    attributes: ['creditNoteNumber'],
  })) as any;

  let seq = 1;
  if (lastNote?.creditNoteNumber) {
    const lastSeqStr = lastNote.creditNoteNumber.slice(-4);
    const lastSeq = parseInt(lastSeqStr, 10);
    if (!isNaN(lastSeq)) {
      seq = lastSeq + 1;
    }
  }

  return seq.toString().padStart(4, '0');
}

/**
 * Sanitize certificate number for use in note numbers (alphanumeric and single hyphens only).
 */
function sanitizeCertificateNumber(raw: string): string {
  const s = (raw || '').trim().replace(/\s+/g, '-').replace(/[^A-Za-z0-9-]/g, '') || '';
  return s || 'XX';
}

/**
 * Form 16 credit note number (16 chars): CN{dc}{fy}{q}{seq}
 * Example: CN00628226Q20001
 */
export async function formatForm16CreditNoteNumber(
  dealerCode: string,
  financialYear: string,
  quarter: string
): Promise<string> {
  const dc = padDealerCode(dealerCode);
  const fy = form16FyShort(financialYear);
  const q = normalizeQuarter(quarter);
  const seq = await getNextForm16NoteSequence(dealerCode, financialYear, quarter);
  return `CN${dc}${fy}${q}${seq}`;
}

/**
 * Form 16 debit note number (16 chars): DN{dc}{fy}{q}{seq}
 * Usually matches the sequence of the credit note being reversed.
 */
export function formatForm16DebitNoteNumber(
  creditNoteNumber: string
): string {
  if (creditNoteNumber.startsWith('CN')) {
    return creditNoteNumber.replace(/^CN/, 'DN');
  }
  return creditNoteNumber.replace(/^.{2}/, 'DN'); // fallback
}

/**
 * Match submission against latest 26AS aggregated amount (quarter-level). Only Section 194Q, Booking F/O.
 * Reject if no 26AS data, amount mismatch, or duplicate (already settled with same amount).
 * On match: create credit note, ledger entry, set quarter status SETTLED.
 */
async function run26asMatchAndCreditNote(submission: Form16aSubmission): Promise<{ validationStatus: string; creditNoteNumber?: string | null; validationNotes?: string }> {
  const sub = submission as any;
  const tanNumberRaw = (sub.tanNumber || '').toString().trim();
  const tanNumber = normalizeTanNumber(tanNumberRaw);
  const tdsAmount = parseFloat(sub.tdsAmount) || 0;

  if (!tanNumber || tanNumber.length < 10 || tdsAmount <= 0) {
    logger.warn(
      `[Form16] 26AS MATCH RESULT: RESUBMISSION_NEEDED – OCR incomplete. Form 16A: TAN=${tanNumber || '(missing)'}, TDS amount=${tdsAmount}, FY=${(sub.financialYear || '').toString().trim() || '(missing)'}, Quarter=${(sub.quarter || '').toString().trim() || '(missing)'}. No 26AS check performed.`
    );
    await submission.update({
      validationStatus: 'resubmission_needed',
      validationNotes: 'OCR data incomplete/invalid (TAN or TDS amount missing). Please resubmit Form 16 or contact RE for manual approval.',
    });
    return { validationStatus: 'resubmission_needed' };
  }

  const financialYearRaw = (sub.financialYear || '').trim();
  const quarterRaw = (sub.quarter || '').trim();
  let financialYear = normalizeFinancialYear(financialYearRaw) || financialYearRaw;
  let quarter = normalizeQuarter(quarterRaw) || quarterRaw;

  if (!financialYear || !quarter) {
    logger.warn(
      `[Form16] 26AS MATCH RESULT: RESUBMISSION_NEEDED – FY or Quarter missing. Form 16A: FY=${financialYearRaw || '(missing)'}, Quarter=${quarterRaw || '(missing)'}, TAN=${tanNumber}, TDS amount=${tdsAmount}. No 26AS check performed.`
    );
    await submission.update({
      validationStatus: 'resubmission_needed',
      validationNotes: 'Financial year or quarter missing. Please resubmit Form 16.',
    });
    return { validationStatus: 'resubmission_needed' };
  }

  const extracted = (sub.ocrExtractedData || {}) as Record<string, unknown>;
  const submittedPan =
    (extracted.panOfDeductee as string) ||
    (extracted.deducteePan as string) ||
    (extracted.panNumber as string) ||
    null;
  const normalizedSubmittedPan = submittedPan ? String(submittedPan).trim().toUpperCase() : null;
  const submittedAmountPaid = toNumberOrNull(extracted.totalAmountPaid ?? sub.totalAmount);
  const submittedTaxDeducted = toNumberOrNull(extracted.totalTaxDeducted ?? sub.tdsAmount);
  const submittedTdsDeposited = toNumberOrNull(extracted.totalTdsDeposited ?? sub.tdsAmount);
  const submittedTransactionDate = normalizeDateOnly(extracted.transactionDate);
  const submittedLastUpdatedOn = normalizeDateOnly(extracted.certificateDate ?? extracted.lastUpdatedOn ?? extracted.lastUpdatedDate);

  // Mandatory for matching: Form 16A "Last updated on" must be extracted and matched to 26AS booking date.
  if (!submittedLastUpdatedOn) {
    const msg = 'OCR could not extract "Last updated on" date from Form 16A. Please resubmit a clear document.';
    await submission.update({
      validationStatus: 'resubmission_needed',
      validationNotes: msg,
    });
    return {
      validationStatus: 'resubmission_needed',
      validationNotes: msg,
    };
  }

  // Latest 26AS upload rows for the same TAN + FY + Quarter.
  let latestRows = await getLatest26asRowsForQuarter(tanNumber, financialYear, quarter);

  // If OCR extracted FY/Quarter incorrectly, derive FY/Quarter from OCR dates and retry.
  if (latestRows.length === 0) {
    const derivedFromTx = deriveFyAndQuarterFromDateOnly(submittedTransactionDate);
    const derivedFromBooking = deriveFyAndQuarterFromDateOnly(submittedLastUpdatedOn);
    const derived = derivedFromTx || derivedFromBooking;
    if (derived && (derived.financialYear !== financialYear || derived.quarter !== quarter)) {
      const altRows = await getLatest26asRowsForQuarter(tanNumber, derived.financialYear, derived.quarter);
      if (altRows.length > 0) {
        logger.warn(
          `[Form16] FY/Quarter retry using OCR date-derived period. TAN=${tanNumber}. OCR FY=${financialYear},Q=${quarter} → derived FY=${derived.financialYear},Q=${derived.quarter}.`
        );
        financialYear = derived.financialYear;
        quarter = derived.quarter;
        latestRows = altRows;
        await submission.update({ financialYear, quarter });
      }
    }
  }

  const aggregated26as = latestRows.reduce((sum, r) => sum + (r.taxDeducted || 0), 0);

  const hasPanColumn = await isPanNumberColumnPresent();
  if (normalizedSubmittedPan && hasPanColumn && latestRows.length > 0) {
    const hasPanMatch = latestRows.some((r) => r.panNumber && r.panNumber === normalizedSubmittedPan);
    if (!hasPanMatch) {
      logger.warn(
        `[Form16] 26AS MATCH RESULT: FAILED – PAN mismatch. TAN=${tanNumber}, PAN(Form16A)=${normalizedSubmittedPan}, FY=${financialYear}, Quarter=${quarter}.`
      );
      await submission.update({
        validationStatus: 'failed',
        validationNotes: `PAN mismatch with latest 26AS for TAN no - ${tanNumber}. Form 16A PAN: ${normalizedSubmittedPan}.`,
      });
      return { validationStatus: 'failed', validationNotes: 'PAN mismatch with latest 26AS.' };
    }
  } else if (normalizedSubmittedPan && !hasPanColumn) {
    logger.warn(
      `[Form16] PAN strict check skipped because DB column tds_26as_entries.pan_number is missing. TAN=${tanNumber}, FY=${financialYear}, Quarter=${quarter}.`
    );
  }

  if (latestRows.length === 0) {
    // Provide actionable debug info so we can see why latestRows became empty
    // (section_code not 194Q, status not F/O, FY/Quarter mismatch, or TAN mismatch).
    let debugNotes = '';
    try {
      const dbg = await get26asCoverageDebug(tanNumber, financialYear, quarter);
      debugNotes = ` | DEBUG 26AS coverage: total=${dbg.totalRows}, matching(194Q & F/O)=${dbg.matchingRows}. Top breakdown: ${dbg.breakdownLines || '(none)'}`;
    } catch (e: any) {
      debugNotes = ` | DEBUG 26AS coverage query failed: ${e?.message || String(e)}`;
    }

    logger.warn(
      `[Form16] 26AS MATCH RESULT: FAILED – No 26AS data. Form 16A: TAN=${tanNumber}, FY=${financialYear}, Quarter=${quarter}, TDS amount=${tdsAmount} | 26AS: no records for this TAN/FY/Quarter (Section 194Q, Booking F/O).${debugNotes}`
    );
    await submission.update({
      validationStatus: 'failed',
      validationNotes: `No 26AS data found for TAN no - ${tanNumber}, financial year and quarter. Please ensure 26AS has been uploaded for this period.${debugNotes}`,
    });
    return {
      validationStatus: 'failed',
      validationNotes: `No 26AS record found for this TAN no - ${tanNumber}, financial year and quarter.${debugNotes}`,
    };
  }

  // Validate against quarter-level aggregate from latest upload.
  // 26AS has many transaction lines; we compare submitted totals against aggregated totals.
  const aggregatedAmountPaid = latestRows.reduce((sum, r) => sum + (r.amountPaid || 0), 0);
  const aggregatedTaxDeducted = latestRows.reduce((sum, r) => sum + (r.taxDeducted || 0), 0);
  const aggregatedTdsDeposited = latestRows.reduce((sum, r) => sum + (r.totalTdsDeposited ?? r.taxDeducted ?? 0), 0);

  if (
    submittedAmountPaid != null &&
    Math.abs(submittedAmountPaid - aggregatedAmountPaid) > AMOUNT_MATCH_TOLERANCE
  ) {
    logger.warn(
      `[Form16] 26AS MATCH RESULT: FAILED – Amount paid mismatch. TAN=${tanNumber}, PAN=${submittedPan || '(not available)'}, FY=${financialYear}, Quarter=${quarter}. Form16A amountPaid=${submittedAmountPaid}, 26AS latest aggregate amountPaid=${aggregatedAmountPaid}.`
    );
    await submission.update({
      validationStatus: 'failed',
      validationNotes:
        `Amount paid mismatch with latest 26AS for TAN no - ${tanNumber}. Form 16A amount paid: ${submittedAmountPaid}. Latest 26AS aggregated amount paid for this quarter: ${aggregatedAmountPaid}.`,
    });
    return { validationStatus: 'failed', validationNotes: 'Amount paid mismatch with latest 26AS.' };
  }

  if (
    submittedTaxDeducted != null &&
    Math.abs(submittedTaxDeducted - aggregatedTaxDeducted) > AMOUNT_MATCH_TOLERANCE
  ) {
    logger.warn(
      `[Form16] 26AS MATCH RESULT: FAILED – Tax deducted mismatch. TAN=${tanNumber}, PAN=${submittedPan || '(not available)'}, FY=${financialYear}, Quarter=${quarter}. Form16A taxDeducted=${submittedTaxDeducted}, 26AS latest aggregate taxDeducted=${aggregatedTaxDeducted}.`
    );
    await submission.update({
      validationStatus: 'failed',
      validationNotes:
        `Tax deducted mismatch with latest 26AS for TAN no - ${tanNumber}. Form 16A tax deducted: ${submittedTaxDeducted}. Latest 26AS aggregated tax deducted for this quarter: ${aggregatedTaxDeducted}.`,
    });
    return { validationStatus: 'failed', validationNotes: 'Tax deducted mismatch with latest 26AS.' };
  }

  if (
    submittedTdsDeposited != null &&
    Math.abs(submittedTdsDeposited - aggregatedTdsDeposited) > AMOUNT_MATCH_TOLERANCE
  ) {
    logger.warn(
      `[Form16] 26AS MATCH RESULT: FAILED – TDS deposited mismatch. TAN=${tanNumber}, PAN=${submittedPan || '(not available)'}, FY=${financialYear}, Quarter=${quarter}. Form16A tdsDeposited=${submittedTdsDeposited}, 26AS latest aggregate tdsDeposited=${aggregatedTdsDeposited}.`
    );
    await submission.update({
      validationStatus: 'failed',
      validationNotes:
        `TDS deposited mismatch with latest 26AS for TAN no - ${tanNumber}. Form 16A TDS deposited: ${submittedTdsDeposited}. Latest 26AS aggregated TDS deposited for this quarter: ${aggregatedTdsDeposited}.`,
    });
    return { validationStatus: 'failed', validationNotes: 'TDS deposited mismatch with latest 26AS.' };
  }

  // Optional date checks: if OCR extracted transaction/booking date, at least one latest-upload row should contain that date.
  if (submittedTransactionDate) {
    const hasTxDate = latestRows.some((r) => normalizeDateOnly(r.transactionDate) === submittedTransactionDate);
    if (!hasTxDate) {
      await submission.update({
        validationStatus: 'failed',
        validationNotes:
          `Transaction date mismatch with latest 26AS for TAN no - ${tanNumber}. No latest 26AS transaction found with date ${submittedTransactionDate}.`,
      });
      return { validationStatus: 'failed', validationNotes: 'Transaction date mismatch with latest 26AS.' };
    }
  }
  // Match Form 16A "Last updated on" against 26AS "Date of Booking"
  const hasBookingDate = latestRows.some((r) => normalizeDateOnly(r.dateOfBooking) === submittedLastUpdatedOn);
  if (!hasBookingDate) {
    await submission.update({
      validationStatus: 'failed',
      validationNotes:
        `Last updated on date mismatch with latest 26AS booking date for TAN no - ${tanNumber}. Form 16A last updated on: ${submittedLastUpdatedOn}.`,
    });
    return { validationStatus: 'failed', validationNotes: 'Last updated on date mismatch with latest 26AS booking date.' };
  }

  if (Math.abs(tdsAmount - aggregated26as) > AMOUNT_MATCH_TOLERANCE) {
    logger.warn(
      `[Form16] 26AS MATCH RESULT: FAILED – Amount mismatch. Form 16A TDS amount=${tdsAmount} | 26AS aggregated amount (quarter)=${aggregated26as} | TAN=${tanNumber}, FY=${financialYear}, Quarter=${quarter}. Difference=${Math.abs(tdsAmount - aggregated26as).toFixed(2)}.`
    );
    await submission.update({
      validationStatus: 'failed',
      validationNotes: `Amount mismatch with latest 26AS for TAN no - ${tanNumber}. Form 16A TDS amount: ${tdsAmount}. Latest 26AS aggregated amount for this quarter: ${aggregated26as}. Please submit Form 16 with correct data.`,
    });
    return {
      validationStatus: 'failed',
      validationNotes: `Amount mismatch with latest 26AS for TAN no - ${tanNumber}. Please verify the certificate and resubmit.`,
    };
  }

  // Duplicate check: quarter already SETTLED with an active credit for this tan+fy+quarter
  const qStatus = await getQuarterStatus(tanNumber, financialYear, quarter);
  if (qStatus && qStatus.status === 'SETTLED' && qStatus.lastCreditNoteId) {
    const lastCn = await Form16CreditNote.findByPk(qStatus.lastCreditNoteId, { attributes: ['amount'] });
    const lastAmount = lastCn ? parseFloat((lastCn as any).amount as string) : 0;
    if (Math.abs(lastAmount - tdsAmount) <= AMOUNT_MATCH_TOLERANCE) {
      logger.warn(
        `[Form16] 26AS MATCH RESULT: DUPLICATE – Quarter already settled. Form 16A TDS amount=${tdsAmount} | Existing credit note amount=${lastAmount} | TAN=${tanNumber}, FY=${financialYear}, Quarter=${quarter}. No new credit note issued.`
      );
      await submission.update({
        validationStatus: 'duplicate',
        validationNotes:
          'This quarter is already settled with the same amount. Duplicate submission rejected. No new credit note issued.',
      });
      return { validationStatus: 'duplicate', validationNotes: 'Credit note already issued for this FY and quarter.' };
    }
  }

  // Dealer code, certificate number and version from submission (for revised 26AS / Form 16 versioning)
  const dealerCode = (sub.dealerCode || '').toString().trim();
  const certificateNumber = (sub.form16aNumber || '').toString().trim();
  const version = typeof sub.version === 'number' && sub.version >= 1 ? sub.version : 1;
  const cnNumber = await formatForm16CreditNoteNumber(dealerCode, financialYear, quarter);
  const now = new Date();
  const creditNote = await Form16CreditNote.create({
    submissionId: submission.id,
    creditNoteNumber: cnNumber,
    amount: tdsAmount,
    issueDate: now,
    financialYear,
    quarter,
    status: 'issued',
    remarks: 'Auto-generated on 26AS match (quarter aggregate).',
    createdAt: now,
    updatedAt: now,
  });
  await addLedgerEntry({
    tanNumber,
    financialYear,
    quarter,
    entryType: 'CREDIT',
    amount: tdsAmount,
    creditNoteId: creditNote.id,
    form16SubmissionId: submission.id,
  });
  await setQuarterStatusSettled(tanNumber, financialYear, quarter, creditNote.id);
  await submission.update({
    validationStatus: 'success',
    validationNotes: null,
  });

  // Push Form 16 credit note incoming CSV to WFM INCOMING/WFM_MAIN/FORM16
  try {
    const trnsUniqNo = `F16-CN-${submission.id}-${creditNote.id}-${Date.now()}`;
    await creditNote.update({ trnsUniqNo });
    const docDate = now.toISOString().slice(0, 10).replace(/-/g, '');
    const fyCompact = form16FyCompact(financialYear) || '';
    const finYearAndQuarter = fyCompact && quarter ? `FY_${fyCompact}_${quarter}` : '';
    const csvRow: Record<string, string | number> = {
      TRNS_UNIQ_NO: trnsUniqNo,
      TDS_TRNS_ID: cnNumber,
      DEALER_CODE: padDealerCode(dealerCode),
      TDS_TRNS_DOC_TYPE: 'ZTDS',
      DLR_TAN_NO: tanNumber,
      'FIN_YEAR&QUARTER': finYearAndQuarter,
      DOC_DATE: docDate,
      TDS_AMT: formatForm16IncomingCsvTdsAmt(tdsAmount, 'credit'),
      TDS_CERTIFICATE_NO: certificateNumber,
    };
    const fileName = `${cnNumber}.csv`;
    await wfmFileService.generateForm16IncomingCSV([csvRow], fileName, 'credit');
    logger.info(`[Form16] Credit note CSV pushed to WFM FORM16: ${cnNumber}`);
  } catch (csvErr: any) {
    logger.error('[Form16] Failed to push credit note CSV to WFM FORM16:', csvErr?.message || csvErr);
    // Do not fail the flow; credit note and ledger are already created
  }

  logger.info(
    `[Form16] 26AS MATCH RESULT: SUCCESS – Credit note issued. Form 16A: TAN=${tanNumber}, FY=${financialYear}, Quarter=${quarter}, TDS amount=${tdsAmount} | 26AS aggregated=${aggregated26as} | Credit note=${cnNumber}.`
  );
  return { validationStatus: 'success', creditNoteNumber: cnNumber };
}

export async function createSubmission(
  userId: string,
  fileBuffer: Buffer,
  originalName: string,
  body: CreateForm16SubmissionBody,
  userEmail?: string | null
): Promise<CreateForm16SubmissionResult> {
  // Dealer: dealer code from users.employee_number (getDealerCodeForUser) or body.dealerCode for RE/UAT. Used for submission, credit note, and debit note.
  const resolvedDealerCode = await getDealerCodeForUser(userId, userEmail);
  const overrideDealerCode = (body.dealerCode || '').trim() || null;
  const dealerCode = resolvedDealerCode || overrideDealerCode;
  // Frontend may not provide dealerCode (dealer code may be absent in Form16/26AS),
  // so we must not hard-fail. Use a deterministic fallback so note generation can continue.
  const effectiveDealerCode = dealerCode || '000000';
  if (!dealerCode) {
    logger.warn(
      '[Form16] dealerCode not resolved for submission; using fallback dealerCode="000000". Matching uses TAN/FY/Quarter, but note numbering/CSV dealer code will be for fallback.'
    );
  }

  const version = await getNextVersionForDealerFyQuarter(effectiveDealerCode, body.financialYear, body.quarter);

  const requestNumber = await generateRequestNumber();
  const title = version > 1
    ? `Form 16A - ${body.financialYear} ${body.quarter} (v${version})`
    : `Form 16A - ${body.financialYear} ${body.quarter}`;
  const description = version > 1
    ? `Form 16A TDS certificate submission. Deductor: ${body.deductorName}. Version ${version}.`
    : `Form 16A TDS certificate submission. Deductor: ${body.deductorName}.`;

  const workflow = await WorkflowRequest.create({
    requestNumber,
    initiatorId: userId,
    templateType: 'FORM_16',
    workflowType: 'FORM_16',
    title,
    description,
    priority: Priority.STANDARD,
    status: WorkflowStatus.PENDING,
    currentLevel: 1,
    totalLevels: 1,
    totalTatHours: 0,
    isDraft: false,
    isDeleted: false,
    isPaused: false,
  });

  const requestId = (workflow as any).requestId;
  let documentUrl: string;
  let uploadFilePath: string;
  let uploadFileName: string;

  try {
    const mimeType = (originalName || '').toLowerCase().endsWith('.pdf') ? 'application/pdf' : 'application/octet-stream';
    const result = await gcsStorageService.uploadFileWithFallback({
      buffer: fileBuffer,
      originalName: originalName || 'form16a.pdf',
      mimeType,
      requestNumber,
      fileType: 'documents',
    });
    documentUrl = result.storageUrl;
    uploadFilePath = result.filePath || result.storageUrl || '';
    uploadFileName = result.fileName || (originalName || 'form16a.pdf');
  } catch (err: any) {
    logger.error('[Form16] Document upload failed:', err);
    await workflow.destroy();
    throw new Error('Failed to upload Form 16A document. Please try again.');
  }

  const now = new Date();
  // Truncate strings to column max lengths to avoid Sequelize validation error
  const safeStr = (s: string, max: number) => (s ?? '').slice(0, max);
  const submission = await Form16aSubmission.create({
    requestId,
    dealerCode: safeStr(effectiveDealerCode, 50),
    form16aNumber: safeStr(body.form16aNumber, 50),
    financialYear: safeStr(body.financialYear, 20),
    quarter: safeStr(body.quarter, 10),
    version,
    tdsAmount: Number(body.tdsAmount) || 0,
    totalAmount: Number(body.totalAmount) || 0,
    tanNumber: safeStr(body.tanNumber, 20),
    deductorName: safeStr(body.deductorName, 255),
    documentUrl,
    ocrExtractedData: body.ocrExtractedData ?? undefined,
    status: 'pending',
    submittedDate: now,
    createdAt: now,
    updatedAt: now,
  });

  // Create a row in documents table so the Form 16 uploaded file appears in the request's Documents tab (dealer, RE, admin).
  const origName = (originalName || 'form16a.pdf').slice(0, 255);
  const ext = (origName.includes('.') ? origName.split('.').pop() : 'pdf') || 'pdf';
  const checksum = crypto.createHash('sha256').update(fileBuffer).digest('hex');
  const docStorageUrl = documentUrl.length <= 500 ? documentUrl : null;
  try {
    await Document.create({
      requestId,
      uploadedBy: userId,
      fileName: uploadFileName.slice(0, 255),
      originalFileName: origName,
      fileType: 'application/pdf',
      fileExtension: ext.slice(0, 10),
      fileSize: fileBuffer.length,
      filePath: uploadFilePath.slice(0, 500),
      storageUrl: docStorageUrl ?? undefined,
      mimeType: 'application/pdf',
      checksum,
      isGoogleDoc: false,
      category: 'SUPPORTING',
      version: 1,
      isDeleted: false,
      downloadCount: 0,
      uploadedAt: now,
    });
  } catch (docErr: any) {
    logger.error('[Form16] Failed to create document row for Documents tab:', docErr);
    // Do not fail the submission; form16a_submissions.document_url is already set
  }

  try {
    const initiator = await User.findByPk(userId, { attributes: ['userId', 'displayName', 'email'], raw: true }) as { userId: string; displayName?: string; email?: string } | null;
    const initiatorName = initiator?.displayName || initiator?.email || 'Dealer';
    await activityService.log({
      requestId,
      type: 'submitted',
      user: { userId, name: initiatorName },
      timestamp: now.toISOString(),
      action: 'Form 16A submitted',
      details: `Form 16A certificate for ${body.financialYear} ${body.quarter} submitted by ${initiatorName}.${version > 1 ? ` (Version ${version})` : ''}`,
    });
  } catch (actErr: any) {
    logger.warn('[Form16] Failed to log activity:', actErr);
  }

  logger.info('[Form16] Submission created', {
    requestId,
    requestNumber,
    submissionId: submission.id,
    dealerCode,
  });

  let validationStatus: string | undefined;
  let creditNoteNumber: string | null | undefined;
  let validationNotes: string | undefined;
  try {
    const matchResult = await run26asMatchAndCreditNote(submission);
    validationStatus = matchResult.validationStatus;
    creditNoteNumber = matchResult.creditNoteNumber ?? null;
    validationNotes = matchResult.validationNotes ?? undefined;
    logger.info(
      `[Form16] Submission match complete: requestId=${requestId}, status=${validationStatus}${validationNotes ? `, notes=${validationNotes}` : ''}${creditNoteNumber ? `, creditNote=${creditNoteNumber}` : ''}.`
    );
    // When credit note is issued (completed), set workflow status to CLOSED so the request appears on Closed requests page
    if (validationStatus === 'success' && creditNoteNumber) {
      logger.info('[Form16] Success path reached; triggering PDF rename', {
        requestId,
        submissionId: submission.id,
        creditNoteNumber,
        uploadFilePath,
      });
      const workflow = await WorkflowRequest.findOne({ where: { requestId }, attributes: ['requestId', 'status'] });
      if (workflow && (workflow as any).status !== WorkflowStatus.CLOSED) {
        await workflow.update({ status: WorkflowStatus.CLOSED });
        logger.info(`[Form16] Request ${requestId} set to CLOSED (credit note issued).`);
      }
      await renameForm16SubmissionPdfAfterCreditNote({
        submissionId: submission.id,
        requestId,
        oldRelativePath: uploadFilePath.replace(/\\/g, '/'),
      });
      logger.info('[Form16] PDF rename call completed', {
        requestId,
        submissionId: submission.id,
      });
    } else {
      logger.info('[Form16] PDF rename not triggered (submission not successful)', {
        requestId,
        submissionId: submission.id,
        validationStatus,
        creditNoteNumber: creditNoteNumber || null,
      });
    }
  } catch (err: any) {
    logger.error(
      `[Form16] 26AS match/credit note error for requestId=${requestId}: Form 16A TAN=${(submission as any).tanNumber}, FY=${(submission as any).financialYear}, Quarter=${(submission as any).quarter}, TDS amount=${(submission as any).tdsAmount}. Error:`,
      err
    );
    validationStatus = 'failed';
    const rawMessage = err?.message || 'Validation error.';
    validationNotes = /notNull|Violation|Sequelize|ECONNREFUSED|database/i.test(rawMessage)
      ? 'Failed - data mismatch with 26AS, submit the Form 16 with correct data.'
      : rawMessage;
    await submission.update({
      validationStatus: 'failed',
      validationNotes,
    });
  }

  return {
    requestId,
    requestNumber,
    submissionId: submission.id,
    validationStatus,
    creditNoteNumber,
    validationNotes,
  };
}

// ---------- RE-only: list all credit notes (all dealers) ----------
export async function listAllCreditNotesForRe(filters?: { financialYear?: string; quarter?: string }) {
  const whereNote: any = {};
  const whereSubmission: any = {};
  if (filters?.financialYear) whereNote.financialYear = filters.financialYear;
  if (filters?.quarter) whereNote.quarter = filters.quarter;

  const { rows, count } = await Form16CreditNote.findAndCountAll({
    where: Object.keys(whereNote).length ? whereNote : undefined,
    include: [
      {
        model: Form16aSubmission,
        as: 'submission',
        attributes: ['id', 'requestId', 'dealerCode', 'form16aNumber', 'financialYear', 'quarter', 'status', 'submittedDate'],
        where: Object.keys(whereSubmission).length ? whereSubmission : undefined,
        required: true,
      },
    ],
    order: [['issueDate', 'DESC'], ['createdAt', 'DESC']],
  });

  let hasTrnsUniqNoColumn = true;
  try {
    await sequelize.query(`SELECT trns_uniq_no FROM form_16_credit_notes LIMIT 1`, { type: QueryTypes.SELECT });
  } catch {
    hasTrnsUniqNoColumn = false;
  }

  const noteIds = rows.map((r) => r.id);
  let sapSet = new Set<number>();
  if (hasTrnsUniqNoColumn && noteIds.length) {
    try {
      const creditNotes = await Form16CreditNote.findAll({
        where: { id: { [Op.in]: noteIds } },
        attributes: ['id', 'creditNoteNumber'],
        raw: true,
      }) as any[];
      const creditNumbers = creditNotes.map((n) => n.creditNoteNumber).filter(Boolean);
      const sapRows = await (Form16SapResponse as any).findAll({
        where: { tdsTransId: { [Op.in]: creditNumbers } },
        attributes: ['tdsTransId'],
        raw: true,
      });
      const available = new Set((sapRows as any[]).map((r) => String(r.tdsTransId)));
      sapSet = new Set(creditNotes.filter((n) => available.has(String(n.creditNoteNumber))).map((n) => Number(n.id)));
    } catch (e: any) {
      logger.warn('[Form16] SAP response lookup failed (will treat as unavailable):', e?.message || e);
      sapSet = new Set<number>();
    }
  }

  const dealerCodes = [...new Set(rows.map((r) => (r as any).submission?.dealerCode).filter(Boolean))] as string[];
  const dealers = dealerCodes.length
    ? await Dealer.findAll({
        where: {
          isActive: true,
          [Op.or]: [
            ...dealerCodes.map((c) => ({ salesCode: c })),
            ...dealerCodes.map((c) => ({ dlrcode: c })),
          ],
        },
        attributes: ['salesCode', 'dlrcode', 'dealership', 'dealerPrincipalName'],
      })
    : [];
  const codeToName = new Map<string, string>();
  for (const d of dealers) {
    const code = (d as any).salesCode || (d as any).dlrcode;
    if (code) codeToName.set(code, (d as any).dealership || (d as any).dealerPrincipalName || code);
  }

  const totalAmount = rows.reduce((sum, r) => sum + (Number(r.amount) || 0), 0);

  return {
    rows: rows.map((r) => {
      const dc = (r as any).submission?.dealerCode;
      return {
        id: r.id,
        creditNoteNumber: r.creditNoteNumber,
        sapDocumentNumber: r.sapDocumentNumber,
        sapResponseAvailable: sapSet.has(r.id),
        amount: r.amount,
        issueDate: r.issueDate,
        financialYear: r.financialYear,
        quarter: r.quarter,
        status: r.status,
        remarks: r.remarks,
        dealerCode: dc,
        dealerName: (dc && codeToName.get(dc)) || dc || '—',
        submission: (r as any).submission
          ? {
              requestId: (r as any).submission.requestId,
              dealerCode: (r as any).submission.dealerCode,
              form16aNumber: (r as any).submission.form16aNumber,
              financialYear: (r as any).submission.financialYear,
              quarter: (r as any).submission.quarter,
              status: (r as any).submission.status,
              submittedDate: (r as any).submission.submittedDate,
            }
          : null,
      };
    }),
    total: count,
    summary: {
      totalCreditNotes: count,
      totalAmount,
      activeDealersCount: dealerCodes.length,
    },
  };
}

/** List credit notes: for dealer returns dealer's only; for RE returns all. */
export async function listCreditNotesDealerOrRe(userId: string, filters?: { financialYear?: string; quarter?: string }) {
  const dealerCode = await getDealerCodeForUser(userId);
  if (dealerCode) {
    return listCreditNotesForDealer(userId, filters);
  }
  return listAllCreditNotesForRe(filters);
}

// ---------- RE-only: list all debit notes (all dealers) ----------
export async function listAllDebitNotesForRe(filters?: { financialYear?: string; quarter?: string }) {
  const whereDebit: any = {};
  if (filters?.financialYear) whereDebit.financialYear = filters.financialYear;
  if (filters?.quarter) whereDebit.quarter = filters.quarter;

  const { rows, count } = await Form16DebitNote.findAndCountAll({
    include: [
      {
        model: Form16CreditNote,
        as: 'creditNote',
        attributes: ['id', 'creditNoteNumber', 'financialYear', 'quarter', 'submissionId'],
        required: true,
        where: Object.keys(whereDebit).length ? whereDebit : undefined,
        include: [
          {
            model: Form16aSubmission,
            as: 'submission',
            attributes: ['dealerCode', 'form16aNumber'],
            required: false,
          },
        ],
      },
    ],
    order: [['issueDate', 'DESC'], ['createdAt', 'DESC']],
  });

  // Mark which debit notes have ingested SAP response CSV available.
  const noteIds = rows.map((r: any) => r.id);
  let sapSet = new Set<number>();
  if (noteIds.length) {
    try {
      const debitNotes = await Form16DebitNote.findAll({
        where: { id: { [Op.in]: noteIds } },
        attributes: ['id', 'debitNoteNumber'],
        raw: true,
      }) as any[];
      const debitNumbers = debitNotes.map((n) => n.debitNoteNumber).filter(Boolean);
      const sapRows = await (Form16SapResponse as any).findAll({
        where: { tdsTransId: { [Op.in]: debitNumbers } },
        attributes: ['tdsTransId'],
        raw: true,
      });
      const available = new Set((sapRows as any[]).map((r) => String(r.tdsTransId)));
      sapSet = new Set(debitNotes.filter((n) => available.has(String(n.debitNoteNumber))).map((n) => Number(n.id)));
    } catch (e: any) {
      logger.warn('[Form16] Debit SAP response lookup failed (will treat as unavailable):', e?.message || e);
      sapSet = new Set<number>();
    }
  }

  const dealerCodes = [...new Set(rows.map((r: any) => r.creditNote?.submission?.dealerCode).filter(Boolean))] as string[];
  const dealers = dealerCodes.length
    ? await Dealer.findAll({
        where: {
          isActive: true,
          [Op.or]: [
            ...dealerCodes.map((c) => ({ salesCode: c })),
            ...dealerCodes.map((c) => ({ dlrcode: c })),
          ],
        },
        attributes: ['salesCode', 'dlrcode', 'dealership', 'dealerPrincipalName'],
      })
    : [];
  const codeToName = new Map<string, string>();
  for (const d of dealers) {
    const code = (d as any).salesCode || (d as any).dlrcode;
    if (code) codeToName.set(code, (d as any).dealership || (d as any).dealerPrincipalName || code);
  }

  const totalAmount = rows.reduce((sum: number, r: any) => sum + (Number(r.amount) || 0), 0);

  return {
    rows: rows.map((r: any) => {
      const dc = r.creditNote?.submission?.dealerCode ?? null;
      return {
        id: r.id,
        debitNoteNumber: r.debitNoteNumber,
        sapDocumentNumber: r.sapDocumentNumber ?? null,
        sapResponseAvailable: sapSet.has(r.id),
        amount: r.amount ?? null,
        issueDate: r.issueDate ?? null,
        status: r.status ?? null,
        financialYear: r.creditNote?.financialYear ?? null,
        quarter: r.creditNote?.quarter ?? null,
        creditNoteNumber: r.creditNote?.creditNoteNumber ?? null,
        dealerCode: dc,
        dealerName: (dc && codeToName.get(dc)) || dc || '—',
        form16aNumber: r.creditNote?.submission?.form16aNumber ?? null,
      };
    }),
    total: count,
    summary: {
      totalDebitNotes: count,
      totalAmount,
      impactedDealersCount: dealerCodes.length,
    },
  };
}

/**
 * List Form 16 submissions for the authenticated dealer (for Pending Submissions page).
 * Optional filter: status = pending | failed | pending,failed (default: pending,failed).
 */
export async function listDealerSubmissions(
  userId: string,
  filters?: { status?: string; financialYear?: string; quarter?: string }
) {
  const dealerCode = await getDealerCodeForUser(userId);
  if (!dealerCode) {
    return [];
  }
  const where: any = { dealerCode };
  if (filters?.financialYear) where.financialYear = filters.financialYear;
  if (filters?.quarter) where.quarter = filters.quarter;
  const statusFilter = (filters?.status || 'pending,failed,completed').toLowerCase();
  const wantPending = statusFilter.includes('pending');
  const wantFailed = statusFilter.includes('failed');
  const wantCompleted = statusFilter.includes('completed');
  const rows = await Form16aSubmission.findAll({
    where,
    attributes: ['id', 'requestId', 'form16aNumber', 'financialYear', 'quarter', 'version', 'status', 'validationStatus', 'validationNotes', 'submittedDate', 'documentUrl', 'totalAmount'],
    order: [['financialYear', 'DESC'], ['quarter', 'DESC'], ['submittedDate', 'DESC']],
  });
  const submissionIds = rows.map((r) => r.id);
  const creditNotesList = submissionIds.length
    ? await Form16CreditNote.findAll({
        where: { submissionId: submissionIds },
        attributes: ['submissionId', 'creditNoteNumber'],
      })
    : [];
  const creditNoteBySubmissionId = new Map<number, string>();
  for (const c of creditNotesList as any[]) {
    if (c.submissionId && c.creditNoteNumber) creditNoteBySubmissionId.set(c.submissionId, c.creditNoteNumber);
  }
  const hasNote = new Set(creditNoteBySubmissionId.keys());

  /** Display status for Form 16: never show "Pending"; use Completed, Resubmission needed, Duplicate submission, Balance mismatch, Failed, Under review. */
  const toDisplayStatus = (hasCreditNote: boolean, validationStatus: string | null, status: string, validationNotes?: string | null): string => {
    if (hasCreditNote) return 'Completed';
    const v = (validationStatus || '').toLowerCase();
    const notes = (validationNotes || '').toLowerCase();
    if (v === 'resubmission_needed') return 'Resubmission needed';
    if (v === 'duplicate') return 'Duplicate';
    if (v === 'manually_approved') return 'Completed';
    if (v === 'failed' || status === 'failed') {
      if (notes.includes('mismatch') || notes.includes('26as') || notes.includes('value')) return 'Balance mismatch';
      if (notes.includes('partial')) return 'Partial extracted data';
      return 'Failed';
    }
    return 'Under review';
  };

  const list: Array<{
    id: number;
    requestId: string;
    form16a_number: string;
    financial_year: string;
    quarter: string;
    version: number;
    version_number?: number;
    status: string;
    display_status: string;
    validation_status: string | null;
    submitted_date: string | null;
    total_amount: number | null;
    credit_note_number: string | null;
    document_url: string | null;
  }> = [];
  for (const r of rows as any[]) {
    const hasCreditNote = hasNote.has(r.id);
    const effectiveStatus = hasCreditNote ? 'completed' : (r.validationStatus === 'failed' || r.status === 'failed' ? 'failed' : 'pending');
    const displayStatus = toDisplayStatus(hasCreditNote, r.validationStatus ?? null, r.status || '', r.validationNotes);
    const creditNoteNumber = creditNoteBySubmissionId.get(r.id) ?? null;
    const totalAmount = r.totalAmount != null ? Number(r.totalAmount) : null;
    if (wantCompleted && effectiveStatus === 'completed') {
      list.push({
        id: r.id,
        requestId: r.requestId,
        form16a_number: r.form16aNumber,
        financial_year: r.financialYear,
        quarter: r.quarter,
        version: r.version ?? 1,
        version_number: r.version ?? 1,
        status: effectiveStatus,
        display_status: displayStatus,
        validation_status: r.validationStatus ?? null,
        submitted_date: r.submittedDate ? new Date(r.submittedDate).toISOString() : null,
        total_amount: totalAmount,
        credit_note_number: creditNoteNumber,
        document_url: r.documentUrl ?? null,
      });
    } else if (wantFailed && effectiveStatus === 'failed') {
      list.push({
        id: r.id,
        requestId: r.requestId,
        form16a_number: r.form16aNumber,
        financial_year: r.financialYear,
        quarter: r.quarter,
        version: r.version ?? 1,
        version_number: r.version ?? 1,
        status: effectiveStatus,
        display_status: displayStatus,
        validation_status: r.validationStatus ?? null,
        submitted_date: r.submittedDate ? new Date(r.submittedDate).toISOString() : null,
        total_amount: totalAmount,
        credit_note_number: creditNoteNumber,
        document_url: r.documentUrl ?? null,
      });
    } else if (wantPending && effectiveStatus === 'pending') {
      list.push({
        id: r.id,
        requestId: r.requestId,
        form16a_number: r.form16aNumber,
        financial_year: r.financialYear,
        quarter: r.quarter,
        version: r.version ?? 1,
        version_number: r.version ?? 1,
        status: effectiveStatus,
        display_status: displayStatus,
        validation_status: r.validationStatus ?? null,
        submitted_date: r.submittedDate ? new Date(r.submittedDate).toISOString() : null,
        total_amount: totalAmount,
        credit_note_number: creditNoteNumber,
        document_url: r.documentUrl ?? null,
      });
    }
  }
  return list;
}

/** Quarter metadata for pending-quarters: audit start = first day of quarter (Indian FY). */
function getQuarterStartDate(financialYear: string, quarter: string): Date | null {
  const match = financialYear.match(/^(\d{4})-(\d{4})$/);
  if (!match) return null;
  const startYear = parseInt(match[1], 10);
  if (quarter === 'Q1') return new Date(startYear, 3, 1); // 1 Apr
  if (quarter === 'Q2') return new Date(startYear, 6, 1); // 1 Jul
  if (quarter === 'Q3') return new Date(startYear, 9, 1); // 1 Oct
  if (quarter === 'Q4') return new Date(startYear + 1, 0, 1); // 1 Jan next year
  return null;
}

/** Due date = 45 days after quarter end (approximate). */
function getQuarterDueDate(financialYear: string, quarter: string): Date | null {
  const start = getQuarterStartDate(financialYear, quarter);
  if (!start) return null;
  const end = new Date(start);
  end.setMonth(end.getMonth() + 3);
  end.setDate(0); // last day of quarter
  const due = new Date(end);
  due.setDate(due.getDate() + 45);
  return due;
}

/**
 * List quarters for which the dealer has not completed Form 16A (no credit note).
 * Only includes FY/quarters that have 26AS data (so future quarters like 2027-28 are not shown until 26AS is uploaded).
 * Returns dealer_name, 26AS start date (when 26AS data was first added for that FY/quarter), and days_since_26as_uploaded.
 */
export async function listDealerPendingQuarters(userId: string) {
  const dealerCode = await getDealerCodeForUser(userId);
  if (!dealerCode) {
    return [];
  }
  const dealer = await Dealer.findOne({
    where: { [Op.or]: [{ salesCode: dealerCode }, { dlrcode: dealerCode }], isActive: true },
    attributes: ['dealership', 'dealerPrincipalName'],
    raw: true,
  });
  const dealerName = dealer
    ? ((dealer as any).dealership || (dealer as any).dealerPrincipalName || dealerCode).trim()
    : dealerCode;

  // Only show quarters that exist in 26AS data (no future/unavailable quarters)
  const twentySixAsRows = await Tds26asEntry.findAll({
    attributes: ['financialYear', 'quarter'],
    raw: true,
  });
  const quarterSet = new Map<string, { financialYear: string; quarter: string }>();
  for (const r of twentySixAsRows as { financialYear: string; quarter: string }[]) {
    const key = `${r.financialYear}|${r.quarter}`;
    if (!quarterSet.has(key)) quarterSet.set(key, { financialYear: r.financialYear, quarter: r.quarter });
  }
  const quarters = Array.from(quarterSet.values());
  if (quarters.length === 0) {
    return [];
  }

  // Earliest 26AS created_at per (financial_year, quarter) = "26AS start date"
  const { QueryTypes } = await import('sequelize');
  const twentySixAsMinDates = (await sequelize.query<{ financial_year: string; quarter: string; min_created: Date }>(
    `SELECT financial_year, quarter, MIN(created_at) AS min_created FROM tds_26as_entries GROUP BY financial_year, quarter`,
    { type: QueryTypes.SELECT }
  ));
  const minDateByKey = new Map<string, Date>();
  for (const row of twentySixAsMinDates) {
    const key = `${row.financial_year}|${row.quarter}`;
    if (row.min_created) minDateByKey.set(key, new Date(row.min_created));
  }

  const dealerSubmissions = await Form16aSubmission.findAll({
    where: { dealerCode },
    attributes: ['id', 'financialYear', 'quarter', 'status', 'validationStatus', 'submittedDate'],
  });
  const submissionIds = dealerSubmissions.map((s) => s.id);
  const withNotes = await Form16CreditNote.findAll({
    where: { submissionId: submissionIds },
    attributes: ['submissionId'],
  });
  const completedSubmissionIds = new Set(withNotes.map((n) => n.submissionId));
  const byKey = new Map<string, { id: number; status: string; validationStatus: string | null; submittedDate: Date | null }>();
  for (const s of dealerSubmissions as any[]) {
    const key = `${s.financialYear}|${s.quarter}`;
    const existing = byKey.get(key);
    if (!existing || (s.submittedDate && (!existing.submittedDate || new Date(s.submittedDate) > (existing.submittedDate as Date)))) {
      byKey.set(key, {
        id: s.id,
        status: s.status || 'pending',
        validationStatus: s.validationStatus ?? null,
        submittedDate: s.submittedDate ? new Date(s.submittedDate) : null,
      });
    }
  }
  const result: Array<{
    financial_year: string;
    quarter: string;
    dealer_name: string;
    has_submission: boolean;
    latest_submission_status: string | null;
    latest_submission_id: number | null;
    audit_start_date: string | null;
    twenty_six_as_start_date: string | null;
    days_remaining: number | null;
    days_overdue: number | null;
    days_since_26as_uploaded: number | null;
  }> = [];
  const now = new Date();
  const oneDayMs = 24 * 60 * 60 * 1000;
  for (const { financialYear, quarter } of quarters) {
    const key = `${financialYear}|${quarter}`;
    const sub = byKey.get(key);
    const hasSubmission = !!sub;
    const hasCreditNote = hasSubmission && completedSubmissionIds.has(sub.id);
    if (hasCreditNote) continue;
    const quarterStart = getQuarterStartDate(financialYear, quarter);
    const dueDate = getQuarterDueDate(financialYear, quarter);
    let daysRemaining: number | null = null;
    let daysOverdue: number | null = null;
    if (dueDate) {
      const diffMs = dueDate.getTime() - now.getTime();
      const diffDays = Math.ceil(diffMs / oneDayMs);
      if (diffDays > 0) daysRemaining = diffDays;
      else daysOverdue = Math.abs(diffDays);
    }
    const twentySixAsMin = minDateByKey.get(key);
    const twentySixAsStartDate = twentySixAsMin ? twentySixAsMin.toISOString().slice(0, 10) : null;
    const daysSince26AsUploaded = twentySixAsMin
      ? Math.floor((now.getTime() - twentySixAsMin.getTime()) / oneDayMs)
      : null;
    result.push({
      financial_year: financialYear,
      quarter,
      dealer_name: dealerName,
      has_submission: hasSubmission,
      latest_submission_status: hasSubmission ? (sub!.validationStatus === 'failed' ? 'failed' : sub!.status) : null,
      latest_submission_id: hasSubmission ? sub!.id : null,
      audit_start_date: quarterStart ? quarterStart.toISOString().slice(0, 10) : null,
      twenty_six_as_start_date: twentySixAsStartDate,
      days_remaining: daysRemaining,
      days_overdue: daysOverdue,
      days_since_26as_uploaded: daysSince26AsUploaded,
    });
  }
  result.sort((a, b) => {
    const aFy = a.financial_year;
    const bFy = b.financial_year;
    if (aFy !== bFy) return bFy.localeCompare(aFy);
    const qOrder = { Q1: 0, Q2: 1, Q3: 2, Q4: 3 };
    return (qOrder[b.quarter as keyof typeof qOrder] ?? 0) - (qOrder[a.quarter as keyof typeof qOrder] ?? 0);
  });
  return result;
}

/**
 * RE only. Cancel a Form 16 submission: set submission status to cancelled and workflow to REJECTED.
 */
export async function cancelForm16Submission(requestId: string, _userId: string) {
  const submission = await Form16aSubmission.findOne({ where: { requestId }, attributes: ['id', 'requestId', 'status'] });
  if (!submission) throw new Error('Form 16 submission not found for this request.');
  if ((submission as any).status === 'cancelled') return { submission, workflow: null };
  await submission.update({ status: 'cancelled' });
  const workflow = await WorkflowRequest.findOne({ where: { requestId }, attributes: ['requestId', 'status', 'conclusionRemark'] });
  if (workflow) {
    await workflow.update({
      status: WorkflowStatus.REJECTED,
      conclusionRemark: (workflow as any).conclusionRemark
        ? `${(workflow as any).conclusionRemark}\n\nSubmission cancelled by RE.`
        : 'Submission cancelled by RE.',
    });
  }
  return { submission, workflow };
}

/**
 * RE only. Mark submission as resubmission needed (e.g. partial OCR).
 */
export async function setForm16ResubmissionNeeded(requestId: string, _userId: string) {
  const submission = await Form16aSubmission.findOne({ where: { requestId } });
  if (!submission) throw new Error('Form 16 submission not found for this request.');
  await submission.update({
    validationStatus: 'resubmission_needed',
    validationNotes: 'RE user marked this submission as resubmission needed. Dealer should resubmit Form 16.',
  });
  return { submission };
}

/** Get credit note linked to a Form 16 request (by requestId). Returns null if none. */
export async function getCreditNoteByRequestId(requestId: string) {
  const submission = await Form16aSubmission.findOne({
    where: { requestId },
    attributes: ['id', 'requestId', 'form16aNumber', 'financialYear', 'quarter', 'status'],
  });
  if (!submission) return null;
  const note = await Form16CreditNote.findOne({
    where: { submissionId: submission.id },
    include: [{ model: Form16aSubmission, as: 'submission', attributes: ['id', 'requestId', 'form16aNumber', 'financialYear', 'quarter'] }],
  });
  if (!note) return null;
  const n = note as any;
  return {
    id: n.id,
    creditNoteNumber: n.creditNoteNumber,
    sapDocumentNumber: n.sapDocumentNumber,
    amount: n.amount,
    issueDate: n.issueDate,
    financialYear: n.financialYear,
    quarter: n.quarter,
    status: n.status,
    remarks: n.remarks,
    submission: n.submission ? { requestId: n.submission.requestId, form16aNumber: n.submission.form16aNumber, financialYear: n.submission.financialYear, quarter: n.submission.quarter } : null,
  };
}

/** Get credit note by id with dealer info, debit note (if withdrawn), and same-dealer history for detail page. */
export async function getCreditNoteById(creditNoteId: number) {
  const note = await Form16CreditNote.findByPk(creditNoteId, {
    include: [
      { model: Form16aSubmission, as: 'submission', attributes: ['id', 'requestId', 'dealerCode', 'form16aNumber', 'financialYear', 'quarter', 'status', 'submittedDate'] },
      { model: Form16DebitNote, as: 'debitNote', required: false, attributes: ['id', 'debitNoteNumber', 'sapDocumentNumber', 'amount', 'issueDate', 'status'] },
    ],
  });
  if (!note || !(note as any).submission) return null;
  const dealerCode = (note as any).submission.dealerCode as string;
  const dealer = await Dealer.findOne({
    where: { [Op.or]: [{ salesCode: dealerCode }, { dlrcode: dealerCode }], isActive: true },
    attributes: ['dealership', 'dealerPrincipalName', 'dealerPrincipalEmailId', 'dpContactNumber'],
  });
  const dealerName = dealer ? ((dealer as any).dealership || (dealer as any).dealerPrincipalName || dealerCode) : dealerCode;
  const dealerEmail = (dealer as any)?.dealerPrincipalEmailId ?? '';
  const dealerContact = (dealer as any)?.dpContactNumber ?? '';

  const submissionIds = await Form16aSubmission.findAll({
    where: { dealerCode },
    attributes: ['id'],
  });
  const sIds = submissionIds.map((s) => s.id);
  const dealerNotes = await Form16CreditNote.findAll({
    where: { submissionId: { [Op.in]: sIds } },
    include: [{ model: Form16aSubmission, as: 'submission', attributes: ['form16aNumber', 'submittedDate'] }],
    order: [['issueDate', 'DESC']],
  });

  const n = note as any;
  const debitNote = n.debitNote
    ? {
        id: n.debitNote.id,
        debitNoteNumber: n.debitNote.debitNoteNumber,
        sapDocumentNumber: n.debitNote.sapDocumentNumber,
        amount: n.debitNote.amount,
        issueDate: n.debitNote.issueDate,
        status: n.debitNote.status,
      }
    : null;

  return {
    creditNote: {
      id: n.id,
      creditNoteNumber: n.creditNoteNumber,
      sapDocumentNumber: n.sapDocumentNumber,
      amount: n.amount,
      issueDate: n.issueDate,
      financialYear: n.financialYear,
      quarter: n.quarter,
      status: n.status,
      remarks: n.remarks,
      submission: {
        requestId: n.submission.requestId,
        form16aNumber: n.submission.form16aNumber,
        financialYear: n.submission.financialYear,
        quarter: n.submission.quarter,
        submittedDate: n.submission.submittedDate,
      },
    },
    debitNote,
    dealerName,
    dealerEmail,
    dealerContact,
    dealerCreditNotes: dealerNotes.map((cn) => {
      const c = cn as any;
      return {
        id: c.id,
        creditNoteNumber: c.creditNoteNumber,
        amount: c.amount,
        issueDate: c.issueDate,
        status: c.status,
        form16aNumber: c.submission?.form16aNumber,
        submittedDate: c.submission?.submittedDate,
      };
    }),
  };
}

export async function getCreditNoteSapResponseUrl(creditNoteId: number): Promise<string | null> {
  // API-backed CSV generation is used now; URL is deterministic when SAP response exists.
  const row = await getCreditNoteSapResponse(creditNoteId);
  return row ? `/api/v1/form16/credit-notes/${creditNoteId}/sap-response/csv` : null;
}

export interface Form16SapResponseView {
  fileName: string | null;
  trnsUniqNo: string | null;
  tdsTransId: string | null;
  sapDocumentNumber: string | null;
  msgTyp: string | null;
  message: string | null;
  docDate: string | null;
  tdsAmt: string | null;
  storageUrl: string | null;
  createdAt: Date | null;
  updatedAt: Date | null;
}

function mapSapResponseView(row: any): Form16SapResponseView {
  return {
    fileName: row?.fileName ?? null,
    trnsUniqNo: row?.trnsUniqNo ?? null,
    tdsTransId: row?.tdsTransId ?? null,
    sapDocumentNumber: row?.docNo ?? null,
    msgTyp: row?.msgTyp ?? null,
    message: row?.message ?? null,
    docDate: null,
    tdsAmt: null,
    storageUrl: null,
    createdAt: row?.createdAt ?? null,
    updatedAt: row?.updatedAt ?? null,
  };
}

export async function getCreditNoteSapResponse(creditNoteId: number): Promise<Form16SapResponseView | null> {
  const cn = await Form16CreditNote.findByPk(creditNoteId, { attributes: ['id', 'creditNoteNumber'] });
  const creditNoteNumber = (cn as any)?.creditNoteNumber;
  if (!creditNoteNumber) return null;
  const row = await (Form16SapResponse as any).findOne({
    where: {
      tdsTransId: creditNoteNumber,
    },
    attributes: ['trnsUniqNo', 'tdsTransId', 'docNo', 'msgTyp', 'message', 'createdAt', 'updatedAt'],
    order: [['createdAt', 'DESC']],
  });
  return row ? mapSapResponseView({ ...row.toJSON(), fileName: `${creditNoteNumber}.csv` }) : null;
}

export async function getDebitNoteSapResponseUrl(debitNoteId: number): Promise<string | null> {
  const row = await getDebitNoteSapResponse(debitNoteId);
  return row ? `/api/v1/form16/debit-notes/${debitNoteId}/sap-response/csv` : null;
}

export async function getDebitNoteSapResponse(debitNoteId: number): Promise<Form16SapResponseView | null> {
  const dn = await Form16DebitNote.findByPk(debitNoteId, { attributes: ['id', 'debitNoteNumber'] });
  const debitNoteNumber = (dn as any)?.debitNoteNumber;
  if (!debitNoteNumber) return null;
  const row = await (Form16SapResponse as any).findOne({
    where: {
      tdsTransId: debitNoteNumber,
    },
    attributes: ['trnsUniqNo', 'tdsTransId', 'docNo', 'msgTyp', 'message', 'createdAt', 'updatedAt'],
    order: [['createdAt', 'DESC']],
  });
  return row ? mapSapResponseView({ ...row.toJSON(), fileName: `${debitNoteNumber}.csv` }) : null;
}

/**
 * Dealer-safe download: dealers can only download their own credit note.
 * RE/Admin (non-dealer users) can download any credit note.
 */
export async function getCreditNoteSapResponseUrlForUser(creditNoteId: number, userId: string): Promise<string | null> {
  const dealerCode = await getDealerCodeForUser(userId);
  if (dealerCode) {
    const note = await Form16CreditNote.findByPk(creditNoteId, {
      attributes: ['id', 'submissionId'],
      include: [{ model: Form16aSubmission, as: 'submission', attributes: ['dealerCode'] }],
    });
    const noteDealerCode = (note as any)?.submission?.dealerCode;
    if (!note || !noteDealerCode || String(noteDealerCode).trim() !== String(dealerCode).trim()) {
      // Hide existence for unauthorized dealer
      throw new Error('Credit note not found');
    }
  }
  return getCreditNoteSapResponseUrl(creditNoteId);
}

export async function getCreditNoteSapResponseForUser(creditNoteId: number, userId: string): Promise<Form16SapResponseView | null> {
  const dealerCode = await getDealerCodeForUser(userId);
  if (dealerCode) {
    const note = await Form16CreditNote.findByPk(creditNoteId, {
      attributes: ['id', 'submissionId'],
      include: [{ model: Form16aSubmission, as: 'submission', attributes: ['dealerCode'] }],
    });
    const noteDealerCode = (note as any)?.submission?.dealerCode;
    if (!note || !noteDealerCode || String(noteDealerCode).trim() !== String(dealerCode).trim()) {
      throw new Error('Credit note not found');
    }
  }
  return getCreditNoteSapResponse(creditNoteId);
}

// ---------- Non-submitted dealers (RE only) ----------
const QUARTERS = ['Q1', 'Q2', 'Q3', 'Q4'] as const;

export interface NonSubmittedDealerRow {
  id: string;
  dealerName: string;
  dealerCode: string;
  email: string;
  phone: string;
  location: string;
  missingQuarters: string[];
  lastSubmissionDate: string | null;
  daysSinceLastSubmission: number | null;
  lastNotifiedDate: string | null;
  lastNotifiedBy: string | null;
  notificationCount: number;
  notificationHistory: Array<{ date: string; notifiedBy: string; method: string }>;
}

export interface NonSubmittedDealersResult {
  summary: { totalDealers: number; nonSubmittedCount: number; neverSubmittedCount: number; overdue90Count: number };
  dealers: NonSubmittedDealerRow[];
}

/**
 * Non-submitted dealers: dealers who have at least one missing Form 16A submission for the selected financial year.
 * For each dealer we return missing quarters (e.g. "Q1 2024-25"), last submission date, days since, and placeholder notification fields.
 */
export async function listNonSubmittedDealers(financialYear?: string): Promise<NonSubmittedDealersResult> {
  const fy = (financialYear || '').trim() || getDefaultFinancialYear();
  const allDealers = await Dealer.findAll({
    where: { isActive: true },
    attributes: ['dealerId', 'salesCode', 'dlrcode', 'dealership', 'dealerPrincipalName', 'dealerPrincipalEmailId', 'dpContactNumber', 'state', 'city'],
  });

  const codeToDealer = new Map<string, (typeof allDealers)[0]>();
  for (const d of allDealers) {
    const code = (d as any).salesCode || (d as any).dlrcode;
    if (code && !codeToDealer.has(code)) codeToDealer.set(code, d);
  }

  const allSubmissions = await Form16aSubmission.findAll({
    attributes: ['dealerCode', 'financialYear', 'quarter', 'submittedDate'],
  });

  const submissionsByCode = new Map<string, Array<{ financialYear: string; quarter: string; submittedDate: Date | null }>>();
  for (const s of allSubmissions) {
    const list = submissionsByCode.get(s.dealerCode) || [];
    list.push({
      financialYear: s.financialYear,
      quarter: s.quarter,
      submittedDate: s.submittedDate || null,
    });
    submissionsByCode.set(s.dealerCode, list);
  }

  const now = new Date();
  const dealers: NonSubmittedDealerRow[] = [];

  for (const [code, d] of codeToDealer) {
    const subs = submissionsByCode.get(code) || [];
    const submittedForFy = new Set(subs.filter((x) => x.financialYear === fy).map((x) => x.quarter));
    const missingQuarters = QUARTERS.filter((q) => !submittedForFy.has(q)).map((q) => `${q} ${fy}`);
    if (missingQuarters.length === 0) continue;

    const lastSub = subs.reduce<Date | null>((best, x) => {
      if (!x.submittedDate) return best;
      const d = new Date(x.submittedDate);
      return !best || d > best ? d : best;
    }, null);
    const lastSubmissionDate = lastSub ? lastSub.toISOString().slice(0, 10) : null;
    const daysSinceLastSubmission = lastSub ? Math.floor((now.getTime() - lastSub.getTime()) / (24 * 60 * 60 * 1000)) : null;

    const dealerName = (d as any).dealership || (d as any).dealerPrincipalName || '—';
    const location = [((d as any).city as string) || '', ((d as any).state as string) || ''].filter(Boolean).join(', ') || '—';

    dealers.push({
      id: (d as any).dealerId,
      dealerName,
      dealerCode: code,
      email: (d as any).dealerPrincipalEmailId || '',
      phone: (d as any).dpContactNumber || '',
      location,
      missingQuarters,
      lastSubmissionDate,
      daysSinceLastSubmission,
      lastNotifiedDate: null,
      lastNotifiedBy: null,
      notificationCount: 0,
      notificationHistory: [],
    });
  }

  const dealerCodes = dealers.map((x) => x.dealerCode);
  if (dealerCodes.length > 0) {
    const notifications = await Form16NonSubmittedNotification.findAll({
      where: { dealerCode: { [Op.in]: dealerCodes }, financialYear: fy },
      order: [['notifiedAt', 'DESC']],
      include: [{ model: User, as: 'notifiedByUser', attributes: ['displayName', 'email'], required: false }],
      raw: false,
    });
    const byDealer = new Map<string, typeof notifications>();
    for (const n of notifications) {
      const code = (n as any).dealerCode;
      if (!byDealer.has(code)) byDealer.set(code, []);
      byDealer.get(code)!.push(n);
    }
    for (const row of dealers) {
      const list = byDealer.get(row.dealerCode) || [];
      if (list.length > 0) {
        const latest = list[0] as any;
        row.lastNotifiedDate = latest.notifiedAt ? new Date(latest.notifiedAt).toISOString().slice(0, 10) : null;
        row.lastNotifiedBy = latest.notifiedByUser?.displayName || (latest.notifiedByUser as any)?.email || null;
        row.notificationCount = list.length;
        row.notificationHistory = list.slice(0, 10).map((n: any) => ({
          date: n.notifiedAt ? new Date(n.notifiedAt).toISOString().slice(0, 10) : '',
          notifiedBy: n.notifiedByUser?.displayName || (n.notifiedByUser as any)?.email || '—',
          method: 'In-app',
        }));
      }
    }
  }

  const neverSubmittedCount = dealers.filter((x) => x.lastSubmissionDate === null).length;
  const overdue90Count = dealers.filter((x) => x.daysSinceLastSubmission != null && x.daysSinceLastSubmission > 90).length;

  return {
    summary: {
      totalDealers: allDealers.length,
      nonSubmittedCount: dealers.length,
      neverSubmittedCount,
      overdue90Count,
    },
    dealers,
  };
}

/**
 * Record that an RE user sent a "submit Form 16" notification to a non-submitted dealer, and send the in-app/push alert.
 * Returns the updated dealer row (with lastNotifiedDate, etc.) or null if dealer not in non-submitted list for that FY.
 */
export async function recordNonSubmittedDealerNotification(
  dealerCode: string,
  financialYear: string,
  userId: string
): Promise<NonSubmittedDealerRow | null> {
  const fy = (financialYear || '').trim() || getDefaultFinancialYear();
  const code = (dealerCode || '').trim();
  if (!code) return null;

  const result = await listNonSubmittedDealers(fy);
  const dealer = result.dealers.find((d) => d.dealerCode === code);
  if (!dealer) return null;

  await Form16NonSubmittedNotification.create({
    dealerCode: code,
    financialYear: fy,
    notifiedBy: userId,
  });

  const dealerRecord = await Dealer.findOne({
    where: { [Op.or]: [{ salesCode: code }, { dlrcode: code }], isActive: true },
    attributes: ['dealerPrincipalEmailId', 'dealership', 'dealerPrincipalName'],
  });
  const email = (dealerRecord as any)?.dealerPrincipalEmailId;
  let targetUserId: string | null = null;
  if (email) {
    const u = await User.findOne({
      where: { email: { [Op.iLike]: email } },
      attributes: ['userId'],
    });
    targetUserId = (u as any)?.userId ?? null;
  }
  if (targetUserId) {
    const { triggerForm16AlertSubmit } = await import('./form16Notification.service');
    const dueDate = `FY ${fy} (as per policy)`;
    const name = (dealerRecord as any)?.dealership || (dealerRecord as any)?.dealerPrincipalName || 'Dealer';
    await triggerForm16AlertSubmit([targetUserId], { name, dueDate });
  }

  const updated = await listNonSubmittedDealers(fy);
  const updatedDealer = updated.dealers.find((d) => d.dealerCode === code) ?? null;
  return updatedDealer;
}

function getDefaultFinancialYear(): string {
  const y = new Date().getFullYear();
  const end = (y + 1).toString().slice(-2);
  return `${y}-${end}`;
}

/**
 * Get user IDs for dealers who have not submitted Form 16 for the given financial year.
 * Used by the alert "submit Form 16" scheduled job.
 */
export async function getDealerUserIdsFromNonSubmittedDealers(financialYear?: string): Promise<string[]> {
  const result = await listNonSubmittedDealers(financialYear);
  const emails = [...new Set(result.dealers.map((d) => d.email).filter(Boolean))].map((e) => e.trim().toLowerCase());
  if (emails.length === 0) return [];
  const users = await User.findAll({
    where: { [Op.or]: emails.map((e) => ({ email: { [Op.iLike]: e } })) },
    attributes: ['userId'],
    raw: true,
  });
  return users.map((u) => (u as any).userId);
}

/**
 * Get dealer user IDs missing a specific FY+quarter submission.
 * Used by quarter-based submit reminders.
 */
export async function getDealerUserIdsMissingQuarter(financialYear: string, quarter: 'Q1' | 'Q2' | 'Q3' | 'Q4'): Promise<string[]> {
  const result = await listNonSubmittedDealers(financialYear);
  const key = `${quarter} ${financialYear}`;
  const emails = [...new Set(result.dealers.filter((d) => (d.missingQuarters || []).includes(key)).map((d) => d.email).filter(Boolean))]
    .map((e) => e.trim().toLowerCase());
  if (emails.length === 0) return [];
  const users = await User.findAll({
    where: { [Op.or]: emails.map((e) => ({ email: { [Op.iLike]: e } })) },
    attributes: ['userId'],
    raw: true,
  });
  return users.map((u) => (u as any).userId);
}

/**
 * Get dealers (initiator user IDs) who have at least one pending Form 16 submission (no credit note yet, request open).
 * Returns one entry per pending request so reminders can include a human-readable request reference.
 */
export async function getDealersWithPendingForm16Submissions(): Promise<{ userId: string; requestId: string; requestNumber: string }[]> {
  const submissions = await Form16aSubmission.findAll({
    attributes: ['id', 'requestId'],
    raw: true,
  });
  const submissionIds = submissions.map((s) => (s as any).id);
  const withCreditNote = new Set(
    submissionIds.length
      ? (await Form16CreditNote.findAll({ where: { submissionId: submissionIds }, attributes: ['submissionId'], raw: true })).map(
          (c) => (c as any).submissionId
        )
      : []
  );
  const pendingRequestIds = submissions.filter((s) => !withCreditNote.has((s as any).id)).map((s) => (s as any).requestId);
  if (pendingRequestIds.length === 0) return [];
  const { WorkflowStatus: WS } = await import('../types/common.types');
  const requests = await WorkflowRequest.findAll({
    where: { requestId: pendingRequestIds, templateType: 'FORM_16', status: { [Op.ne]: WS.CLOSED } },
    attributes: ['requestId', 'requestNumber', 'initiatorId'],
    raw: true,
  });
  return requests.map((r) => ({
    userId: (r as any).initiatorId,
    requestId: (r as any).requestId,
    requestNumber: (r as any).requestNumber || (r as any).requestId,
  }));
}

// ---------- 26AS (RE admin) ----------
const DEFAULT_PAGE_SIZE = 50;
const MAX_PAGE_SIZE = 500;

export interface List26asFilters {
  financialYear?: string;
  quarter?: string;
  tanNumber?: string;
  search?: string;
  status?: string;
  assessmentYear?: string;
  sectionCode?: string;
  limit?: number;
  offset?: number;
}

export interface List26asSummary {
  totalRecords: number;
  booked: number;
  notBooked: number;
  pending: number;
  totalTaxDeducted: number;
}

function build26asWhere(filters?: List26asFilters): Record<string, unknown> {
  const where: Record<string, unknown> = {};
  const andClauses: unknown[] = [];

  if (filters?.financialYear) where.financialYear = normalizeFinancialYear(filters.financialYear) || filters.financialYear;
  if (filters?.quarter) where.quarter = normalizeQuarter(filters.quarter) || filters.quarter;
  if (filters?.status) where.statusOltas = filters.status;
  if (filters?.assessmentYear) where.assessmentYear = filters.assessmentYear;
  if (filters?.sectionCode) where.sectionCode = filters.sectionCode;

  if (filters?.tanNumber?.trim()) {
    const normalizedTan = normalizeTanNumber(filters.tanNumber);
    if (normalizedTan) {
      andClauses.push(
        sqlWhere(
          fn('upper', fn('regexp_replace', fn('coalesce', col('tan_number'), ''), '[^a-zA-Z0-9]', '', 'g')),
          { [Op.like]: `%${normalizedTan}%` }
        )
      );
    }
  }

  if (filters?.search?.trim()) {
    const s = filters.search.trim();
    const normalizedSearchTan = normalizeTanNumber(s);
    const searchOr: unknown[] = [{ deductorName: { [Op.iLike]: `%${s}%` } }];
    if (normalizedSearchTan) {
      searchOr.push(
        sqlWhere(
          fn('upper', fn('regexp_replace', fn('coalesce', col('tan_number'), ''), '[^a-zA-Z0-9]', '', 'g')),
          { [Op.like]: `%${normalizedSearchTan}%` }
        )
      );
    }
    andClauses.push({ [Op.or]: searchOr });
  }

  if (andClauses.length > 0) {
    (where as any)[Op.and] = andClauses;
  }
  return where;
}

export async function list26asEntries(filters?: List26asFilters): Promise<{
  rows: Tds26asEntry[];
  total: number;
  summary: List26asSummary;
}> {
  const where = build26asWhere(filters);
  // Use Reflect.ownKeys so symbol keys like Op.and are counted.
  const hasWhere = Reflect.ownKeys(where).length > 0;
  const limit = Math.min(MAX_PAGE_SIZE, Math.max(1, filters?.limit ?? DEFAULT_PAGE_SIZE));
  const offset = Math.max(0, filters?.offset ?? 0);

  const [rowsResult, summaryRows] = await Promise.all([
    Tds26asEntry.findAndCountAll({
      where: hasWhere ? where : undefined,
      order: [['financialYear', 'DESC'], ['quarter', 'ASC'], ['createdAt', 'DESC']],
      limit,
      offset,
    }),
    Tds26asEntry.findAll({
      where: hasWhere ? where : undefined,
      attributes: [
        'statusOltas',
        [fn('COUNT', col('id')), 'count'],
        [fn('SUM', col('tax_deducted')), 'totalTax'],
      ],
      group: ['statusOltas'],
      raw: true,
    }) as unknown as Promise<Array<{ statusOltas?: string | null; status_oltas?: string | null; count: string; totalTax: string | null }>>,
  ]);

  const { rows, count: total } = rowsResult;
  const summary: List26asSummary = {
    totalRecords: total,
    booked: 0,
    notBooked: 0,
    pending: 0,
    totalTaxDeducted: 0,
  };
  for (const row of summaryRows) {
    const status = (row.statusOltas ?? row.status_oltas ?? '').trim().toUpperCase().slice(0, 1);
    const cnt = parseInt(String(row.count), 10) || 0;
    const tax = parseFloat(String(row.totalTax || 0)) || 0;
    if (status === 'F') summary.booked += cnt;
    else if (status === 'O') summary.notBooked += cnt;
    else if (status === 'P') summary.pending += cnt;
    summary.totalTaxDeducted += tax;
  }
  return { rows, total, summary };
}

export async function create26asEntry(data: {
  panNumber?: string;
  tanNumber: string;
  deductorName?: string;
  quarter: string;
  assessmentYear?: string;
  financialYear: string;
  sectionCode?: string;
  amountPaid?: number;
  taxDeducted: number;
  totalTdsDeposited?: number;
  natureOfPayment?: string;
  transactionDate?: string;
  dateOfBooking?: string;
  statusOltas?: string;
  remarks?: string;
}) {
  const includePanNumber = await isPanNumberColumnPresent();
  const payload: any = {
    tanNumber: data.tanNumber,
    deductorName: data.deductorName,
    quarter: data.quarter,
    assessmentYear: data.assessmentYear,
    financialYear: data.financialYear,
    sectionCode: data.sectionCode,
    amountPaid: data.amountPaid,
    taxDeducted: data.taxDeducted ?? 0,
    totalTdsDeposited: data.totalTdsDeposited,
    natureOfPayment: data.natureOfPayment,
    transactionDate: data.transactionDate,
    dateOfBooking: data.dateOfBooking,
    statusOltas: data.statusOltas,
    remarks: data.remarks,
  };
  if (includePanNumber && data.panNumber != null) payload.panNumber = data.panNumber;

  const entry = await Tds26asEntry.create(payload);
  return entry;
}

export async function update26asEntry(
  id: number,
  data: Partial<{
    panNumber: string;
    tanNumber: string;
    deductorName: string;
    quarter: string;
    assessmentYear: string;
    financialYear: string;
    sectionCode: string;
    amountPaid: number;
    taxDeducted: number;
    totalTdsDeposited: number;
    natureOfPayment: string;
    transactionDate: string;
    dateOfBooking: string;
    statusOltas: string;
    remarks: string;
  }>
) {
  const entry = await Tds26asEntry.findByPk(id);
  if (!entry) return null;
  if (data.panNumber != null) {
    const includePanNumber = await isPanNumberColumnPresent();
    if (!includePanNumber) delete (data as any).panNumber;
  }
  await entry.update(data);
  return entry;
}

export async function delete26asEntry(id: number): Promise<boolean> {
  const entry = await Tds26asEntry.findByPk(id);
  if (!entry) return false;
  await entry.destroy();
  return true;
}

// ---------- 26AS bulk upload from TXT file ----------
/** Supports (1) Official 26AS/Annual Tax Statement format (delimiter ^, deductor blocks + transaction lines) and (2) generic delimiter (comma, tab, pipe, semicolon) with optional header. */

function parseDecimal(val: string): number | undefined {
  if (val == null || val === '') return undefined;
  const n = parseFloat(String(val).replace(/,/g, ''));
  return Number.isNaN(n) ? undefined : n;
}

const MONTH_NAMES: Record<string, number> = { jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6, jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12 };

function parseDateOnly(val: string): string | undefined {
  if (val == null || val === '') return undefined;
  const s = String(val).trim();
  if (!s) return undefined;
  const m = s.match(/^(\d{1,2})-(\w{3})-(\d{4})$/i);
  if (m) {
    const month = MONTH_NAMES[m[2].toLowerCase()];
    if (month) return `${m[3]}-${String(month).padStart(2, '0')}-${m[1].padStart(2, '0')}`;
  }
  return s;
}

/** Derive FY and quarter from date string DD-MMM-YYYY (e.g. 30-Sep-2024) */
function dateToFyAndQuarter(dateStr: string): { financialYear: string; quarter: string } {
  const m = dateStr.match(/^(\d{1,2})-(\w{3})-(\d{4})$/i);
  if (!m) return { financialYear: getCurrentFinancialYear(), quarter: 'Q1' };
  const month = m[2].toLowerCase();
  const year = parseInt(m[3], 10);
  let quarter = 'Q1';
  if (['apr', 'may', 'jun'].includes(month)) quarter = 'Q1';
  else if (['jul', 'aug', 'sep'].includes(month)) quarter = 'Q2';
  else if (['oct', 'nov', 'dec'].includes(month)) quarter = 'Q3';
  else quarter = 'Q4';
  const fyEnd = month === 'jan' || month === 'feb' || month === 'mar' ? year : year + 1;
  const fyStart = fyEnd - 1;
  const next = (fyEnd % 100).toString().padStart(2, '0');
  return { financialYear: `${fyStart}-${next}`, quarter };
}

function getCurrentFinancialYear(): string {
  const y = new Date().getFullYear();
  const next = (y + 1) % 100;
  return `${y}-${next < 10 ? '0' + next : next}`;
}

/** Official 26AS format: lines with ^ delimiter. Deductor summary: "1^Name^TAN^^^^^TotalAmt^Tax^TDS". Transaction: "^1^194Q^30-Sep-2024^F^24-Oct-2024^-^Amount^Tax^TDS". */
function parse26asOfficialFormat(lines: string[]): { rows: any[]; errors: string[] } {
  const errors: string[] = [];
  const rows: any[] = [];
  let currentPAN = '';
  let currentTAN = '';
  let currentDeductorName = '';
  const transactionDateRe = /^\d{1,2}-\w{3}-\d{4}$/i;

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    const cells = line.split('^').map((c) => c.trim());
    if (cells.length < 8) continue;

    const c0 = cells[0];
    const c1 = cells[1];
    const c2 = cells[2];
    const c3 = cells[3];

    // Header block with PAN row:
    // File Creation Date ^ Permanent Account Number (PAN) ^ ...
    // 25-10-2024 ^ AAACE3883A ^ ...
    if (cells.length >= 2 && /^[A-Z]{5}[0-9]{4}[A-Z]$/i.test(c1 || '')) {
      currentPAN = String(c1).trim().toUpperCase();
      continue;
    }

    // Deductor summary: first cell is numeric (Sr No), third looks like TAN (e.g. AGRA13250G), then empties then amounts
    const srNoNum = /^\d+$/.test(c0);
    const looksLikeTan = c2 && c2.length >= 8 && /^[A-Z0-9]+$/i.test(c2);
    if (srNoNum && looksLikeTan && c1) {
      currentTAN = c2;
      currentDeductorName = c1;
      continue;
    }

    // Transaction line: first cell empty (line starts with ^), second is Sr No, third is Section (e.g. 194Q), fourth is Transaction Date
    const firstEmpty = !c0;
    const secondNumeric = /^\d+$/.test(c1);
    const sectionLike = c2 && /^\d{3}[A-Z]?$/i.test(c2);
    const hasDate = c3 && transactionDateRe.test(c3);
    if (firstEmpty && secondNumeric && sectionLike && hasDate && currentTAN) {
      const amountPaid = parseDecimal(cells[7]);
      const taxDeducted = parseDecimal(cells[8]);
      const totalTds = parseDecimal(cells[9]);
      const { financialYear, quarter } = dateToFyAndQuarter(cells[3]);
      rows.push({
        panNumber: currentPAN || undefined,
        tanNumber: currentTAN,
        deductorName: currentDeductorName || undefined,
        quarter,
        financialYear,
        sectionCode: c2 || undefined,
        amountPaid: amountPaid ?? undefined,
        taxDeducted: taxDeducted ?? 0,
        totalTdsDeposited: totalTds ?? undefined,
        natureOfPayment: undefined,
        transactionDate: parseDateOnly(cells[3]),
        dateOfBooking: parseDateOnly(cells[5]),
        assessmentYear: undefined,
        statusOltas: cells[4] || undefined,
        remarks: cells[6] || undefined,
      });
    }
  }
  return { rows, errors };
}

function detectDelimiter(line: string): string {
  const caretCount = (line.match(/\^/g) || []).length;
  if (caretCount >= 5) return '^';
  const candidates = [',', '\t', '|', ';'];
  let best = '\t';
  let maxCols = 0;
  for (const d of candidates) {
    const count = line.split(d).length;
    if (count > maxCols && count >= 2) {
      maxCols = count;
      best = d;
    }
  }
  return best;
}

function looksLikeHeader(line: string): boolean {
  const lower = line.toLowerCase();
  const tokens = lower.split(/[\t,|;]+/).map((s) => s.replace(/\s+/g, ''));
  const hasTan = tokens.some((t) => t.includes('tan') && !t.includes('amount'));
  const hasQuarter = tokens.some((t) => t.includes('quarter') || t === 'q');
  const hasFinancial = tokens.some((t) => t.includes('financial') || t.includes('year') || t === 'fy');
  const hasTax = tokens.some((t) => t.includes('tax') || t.includes('deduct'));
  const hasAmount = tokens.some((t) => t.includes('amount') || t.includes('paid'));
  return (hasTan || hasQuarter || hasFinancial || hasTax || hasAmount) && tokens.length >= 2;
}

function buildColumnMap(headerCells: string[]): Record<string, number> {
  const map: Record<string, number> = {};
  const norm = (s: string) => s.toLowerCase().replace(/[\s_-]+/g, '');
  headerCells.forEach((cell, idx) => {
    const n = norm(cell);
    if (n === 'pan' || n.includes('pannumber') || (n.includes('permanent') && n.includes('account'))) map['panNumber'] = idx;
    if (n.includes('tan') && !n.includes('amount')) map['tanNumber'] = idx;
    else if (n.includes('deductor') && (n.includes('name') || n.length < 20)) map['deductorName'] = idx;
    else if (n.includes('quarter') || n === 'q') map['quarter'] = idx;
    else if (n.includes('financial') || n.includes('year') || n === 'fy') map['financialYear'] = idx;
    else if (n.includes('assessment')) map['assessmentYear'] = idx;
    else if (n.includes('section')) map['sectionCode'] = idx;
    else if (n.includes('amount') && n.includes('paid')) map['amountPaid'] = idx;
    else if ((n.includes('tax') && n.includes('deduct')) || n === 'taxdeducted') map['taxDeducted'] = idx;
    else if (n.includes('total') && n.includes('tds')) map['totalTdsDeposited'] = idx;
    else if (n.includes('nature') && n.includes('payment')) map['natureOfPayment'] = idx;
    else if (n.includes('transaction') && n.includes('date')) map['transactionDate'] = idx;
    else if (n.includes('booking') || n.includes('dateofbooking')) map['dateOfBooking'] = idx;
    else if (n.includes('status') || n.includes('oltas')) map['statusOltas'] = idx;
    else if (n.includes('remark')) map['remarks'] = idx;
  });
  return map;
}

export function parse26asTxtFile(buffer: Buffer): { rows: any[]; errors: string[] } {
  const text = buffer.toString('utf8');
  const rawLines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
  const errors: string[] = [];
  if (rawLines.length === 0) return { rows: [], errors };

  // Lightweight, non-blocking sanity logging – detect obviously suspicious uploads without rejecting them.
  try {
    const totalLines = rawLines.length;
    const sampleLines = rawLines.slice(0, Math.min(200, totalLines));
    const caretLines = sampleLines.filter((l) => (l.match(/\^/g) || []).length >= 5).length;
    const hasDatePattern = sampleLines.some((l) => /\b\d{1,2}-[A-Za-z]{3}-\d{4}\b/.test(l));
    const hasTanLike = sampleLines.some((l) => /\b[A-Z]{4}[A-Z0-9]{5}[A-Z]\b/i.test(l));
    const suspicious =
      totalLines < 5 ||
      (caretLines === 0 && !hasDatePattern && !hasTanLike);
    if (suspicious) {
      logger.warn(
        '[Form16] 26AS TXT upload appears suspicious (non-blocking): ' +
          `lines=${totalLines}, caretLines=${caretLines}, hasDatePattern=${hasDatePattern}, hasTanLike=${hasTanLike}`
      );
    }
  } catch {
    // Never block parsing due to logging issues
  }

  const firstLine = rawLines[0];
  let delimiter = detectDelimiter(firstLine);
  if (delimiter !== '^') {
    const withCaret = rawLines.slice(0, 30).find((l) => (l.match(/\^/g) || []).length >= 5);
    if (withCaret) delimiter = '^';
  }
  if (delimiter === '^') {
    return parse26asOfficialFormat(rawLines);
  }

  const rows: any[] = [];
  const allLines = rawLines.map((l) => l.split(delimiter).map((c) => c.trim()));
  let dataStart = 0;
  let colMap: Record<string, number> = {};
  const numCols = allLines[0].length;

  if (looksLikeHeader(firstLine)) {
    dataStart = 1;
    colMap = buildColumnMap(allLines[0].map((c) => c || ''));
    if (Object.keys(colMap).length === 0) {
      for (let i = 0; i < Math.min(14, numCols); i++) colMap[['tanNumber', 'deductorName', 'quarter', 'financialYear', 'sectionCode', 'amountPaid', 'taxDeducted', 'totalTdsDeposited', 'natureOfPayment', 'transactionDate', 'dateOfBooking', 'assessmentYear', 'statusOltas', 'remarks'][i]] = i;
    }
  } else {
    for (let i = 0; i < Math.min(14, numCols); i++) colMap[['tanNumber', 'deductorName', 'quarter', 'financialYear', 'sectionCode', 'amountPaid', 'taxDeducted', 'totalTdsDeposited', 'natureOfPayment', 'transactionDate', 'dateOfBooking', 'assessmentYear', 'statusOltas', 'remarks'][i]] = i;
  }

  const get = (cells: string[], key: string): string => {
    const idx = colMap[key];
    if (idx == null || idx >= cells.length) return '';
    return String(cells[idx] ?? '').trim();
  };

  const defaultFY = getCurrentFinancialYear();
  for (let i = dataStart; i < allLines.length; i++) {
    const cells = allLines[i];
    if (cells.length < 2) continue;
    const tanNumber = get(cells, 'tanNumber') || (cells[0] ? String(cells[0]).trim() : '') || 'UNKNOWN';
    const quarter = get(cells, 'quarter') || 'Q1';
    const financialYear = get(cells, 'financialYear') || defaultFY;
    const taxDeductedNum = parseDecimal(get(cells, 'taxDeducted')) ?? 0;
    rows.push({
      panNumber: get(cells, 'panNumber') || undefined,
      tanNumber,
      deductorName: get(cells, 'deductorName') || undefined,
      quarter,
      financialYear,
      sectionCode: get(cells, 'sectionCode') || undefined,
      amountPaid: parseDecimal(get(cells, 'amountPaid')),
      taxDeducted: taxDeductedNum,
      totalTdsDeposited: parseDecimal(get(cells, 'totalTdsDeposited')),
      natureOfPayment: get(cells, 'natureOfPayment') || undefined,
      transactionDate: parseDateOnly(get(cells, 'transactionDate')),
      dateOfBooking: parseDateOnly(get(cells, 'dateOfBooking')),
      assessmentYear: get(cells, 'assessmentYear') || undefined,
      statusOltas: get(cells, 'statusOltas') || undefined,
      remarks: get(cells, 'remarks') || undefined,
    });
  }
  return { rows, errors };
}

/** Allowed fields for 26AS create – exclude timestamps so Sequelize sets them. */
const TDS_26AS_CREATE_KEYS = [
  'panNumber', 'tanNumber', 'deductorName', 'quarter', 'financialYear', 'sectionCode',
  'amountPaid', 'taxDeducted', 'totalTdsDeposited', 'natureOfPayment',
  'transactionDate', 'dateOfBooking', 'assessmentYear', 'statusOltas', 'remarks', 'uploadLogId',
] as const;

function build26asCreatePayload(
  row: Record<string, unknown>,
  uploadLogId: number | null | undefined,
  includePanNumber: boolean
): Record<string, unknown> {
  const payload: Record<string, unknown> = {};
  for (const k of TDS_26AS_CREATE_KEYS) {
    if (k === 'uploadLogId') continue;
    if (k === 'panNumber' && !includePanNumber) continue;
    const v = row[k];
    if (v !== undefined && v !== null) payload[k] = v;
  }
  payload.tanNumber = normalizeTanNumber(row.tanNumber);
  if (includePanNumber && row.panNumber != null) payload.panNumber = String(row.panNumber).trim().toUpperCase();
  const rawFy = (row.financialYear != null ? String(row.financialYear).trim() : '') || '';
  const rawQ = (row.quarter != null ? String(row.quarter).trim() : '') || 'Q1';
  payload.financialYear = normalizeFinancialYear(rawFy) || rawFy;
  payload.quarter = normalizeQuarter(rawQ) || rawQ;
  payload.taxDeducted = row.taxDeducted ?? 0;
  if (uploadLogId != null) payload.uploadLogId = uploadLogId;
  return payload;
}

const TDS_26AS_BATCH_SIZE = 500;

/**
 * Upload 26AS TXT file: parse and bulk-insert. Only records with Section 194Q and Booking Status F/O are used for aggregation.
 * All records are stored; pass uploadLogId when log was created first (e.g. by controller) for snapshot/debit processing.
 */
export async function upload26asFile(buffer: Buffer, uploadLogId?: number | null): Promise<{ imported: number; errors: string[] }> {
  const { rows, errors: parseErrors } = parse26asTxtFile(buffer);
  if (rows.length === 0 && parseErrors.length === 0) {
    return { imported: 0, errors: ['File is empty or has no data rows.'] };
  }
  const insertErrors: string[] = [];
  let imported = 0;

  const includePanNumber = await isPanNumberColumnPresent();
  for (let i = 0; i < rows.length; i += TDS_26AS_BATCH_SIZE) {
    const batch = rows.slice(i, i + TDS_26AS_BATCH_SIZE);
    const payloads = batch.map((r) => build26asCreatePayload(r as Record<string, unknown>, uploadLogId, includePanNumber));
    try {
      const created = await Tds26asEntry.bulkCreate(payloads as any[], { validate: true });
      imported += created.length;
    } catch (err: any) {
      const baseRow = i + 1;
      insertErrors.push(`Rows ${baseRow}-${baseRow + batch.length - 1}: ${err?.message || 'Insert failed'}`);
    }
  }
  return {
    imported,
    errors: [...parseErrors, ...insertErrors],
  };
}

/**
 * After 26AS upload: compute quarter aggregates (Section 194Q, Booking F/O only), create snapshots, issue debits when 26AS total changed and quarter was SETTLED.
 * Duplicate 26AS (same total) creates no new snapshot and no debit.
 */
export async function process26asUploadAggregation(uploadLogId: number): Promise<{ snapshotsCreated: number; debitsCreated: number }> {
  const entries = await Tds26asEntry.findAll({
    where: { uploadLogId },
    attributes: ['tanNumber', 'financialYear', 'quarter'],
    raw: true,
  });
  const keys = new Map<string, { tanNumber: string; financialYear: string; quarter: string }>();
  for (const e of entries as any[]) {
    const tan = (e.tanNumber || '').trim().replace(/\s+/g, ' ');
    const fy = (e.financialYear || '').trim();
    const q = (e.quarter || '').trim();
    if (!tan || !fy || !q) continue;
    keys.set(`${tan}|${fy}|${q}`, { tanNumber: tan, financialYear: fy, quarter: q });
  }
  let snapshotsCreated = 0;
  let debitsCreated = 0;
  for (const [, { tanNumber, financialYear, quarter }] of keys) {
    const fy = normalizeFinancialYear(financialYear) || financialYear;
    const q = normalizeQuarter(quarter) || quarter;
    const newTotal = await getLatest26asAggregatedForQuarter(tanNumber, fy, q);
    const latest = await getLatest26asSnapshot(tanNumber, fy, q);
    const prevTotal = latest ? parseFloat(String((latest as any).aggregatedAmount ?? 0)) : 0;
    if (Math.abs(newTotal - prevTotal) < 0.01) continue; // duplicate 26AS: no change
    const status = await getQuarterStatus(tanNumber, fy, q);
    if (status?.status === 'SETTLED' && status.lastCreditNoteId) {
      const creditNote = await Form16CreditNote.findByPk(status.lastCreditNoteId, {
        attributes: ['id', 'amount', 'financialYear', 'quarter', 'creditNoteNumber', 'submissionId', 'issueDate'],
      });
      if (creditNote) {
        const amount = parseFloat(String((creditNote as any).amount ?? 0));
        const submission = await Form16aSubmission.findByPk((creditNote as any).submissionId, { attributes: ['dealerCode', 'version', 'form16aNumber'] });
        // Dealer code, version and certificate number from submission (DN uses same cert as the credit note being reversed)
        const dealerCode = submission ? ((submission as any).dealerCode || '').toString().trim() : '';
        const version = typeof (submission as any)?.version === 'number' && (submission as any).version >= 1 ? (submission as any).version : 1;
        const creditNoteCertNumber = submission ? ((submission as any).form16aNumber || '').toString().trim() : '';
        const cnFy = (creditNote as any).financialYear || fy;
        const cnQuarter = (creditNote as any).quarter || q;
        const creditNoteNumber = (creditNote as any).creditNoteNumber || (creditNote as any).credit_note_number || '';
        const debitNum = formatForm16DebitNoteNumber(creditNoteNumber);
        const now = new Date();
        const debit = await Form16DebitNote.create({
          creditNoteId: creditNote.id,
          debitNoteNumber: debitNum,
          amount,
          issueDate: now,
          status: 'issued',
          reason: '26AS quarter total changed; previous credit reversed.',
          createdAt: now,
          updatedAt: now,
        });
        await addLedgerEntry({
          tanNumber,
          financialYear: fy,
          quarter: q,
          entryType: 'DEBIT',
          amount,
          debitNoteId: debit.id,
        });
        await setQuarterStatusDebitIssued(tanNumber, fy, q, debit.id);
        debitsCreated++;

        // Push Form 16 debit note CSV to WFM INCOMING/WFM_MAIN/FORM16
        try {
          const trnsUniqNo = `F16-DN-${creditNote.id}-${debit.id}-${Date.now()}`;
          await debit.update({ trnsUniqNo });
          const docDate = now.toISOString().slice(0, 10).replace(/-/g, '');
          const fyCompact = form16FyCompact(cnFy) || '';
          const finYearAndQuarter = fyCompact && cnQuarter ? `FY_${fyCompact}_${cnQuarter}` : '';
          const csvRow: Record<string, string | number> = {
            TRNS_UNIQ_NO: trnsUniqNo,
            TDS_TRNS_ID: debitNum,
            DEALER_CODE: padDealerCode(dealerCode),
            TDS_TRNS_DOC_TYPE: 'ZTDS',
            DLR_TAN_NO: tanNumber,
            'FIN_YEAR&QUARTER': finYearAndQuarter,
            DOC_DATE: docDate,
            TDS_AMT: formatForm16IncomingCsvTdsAmt(Number(amount), 'debit'),
            TDS_CERTIFICATE_NO: creditNoteCertNumber,
          };
          const fileName = `${debitNum}.csv`;
          await wfmFileService.generateForm16IncomingCSV([csvRow], fileName, 'debit');
          logger.info(`[Form16] Debit note CSV pushed to WFM FORM16: ${debitNum}`);
        } catch (csvErr: any) {
          logger.error('[Form16] Failed to push debit note CSV to WFM FORM16:', csvErr?.message || csvErr);
        }
      }
    }
    const normalized = (tanNumber || '').trim().replace(/\s+/g, ' ');
    await Form1626asQuarterSnapshot.create({
      tanNumber: normalized,
      financialYear: fy,
      quarter: q,
      aggregatedAmount: newTotal,
      uploadLogId,
      createdAt: new Date(),
    });
    snapshotsCreated++;
  }
  return { snapshotsCreated, debitsCreated };
}

/** Log a 26AS file upload for audit (who uploaded, when, how many records). */
export async function log26asUpload(
  userId: string,
  opts: { fileName?: string | null; recordsImported: number; errorsCount: number }
): Promise<Form1626asUploadLog> {
  const log = await Form1626asUploadLog.create({
    uploadedAt: new Date(),
    uploadedBy: userId,
    fileName: opts.fileName ?? null,
    recordsImported: opts.recordsImported,
    errorsCount: opts.errorsCount,
  });
  return log as Form1626asUploadLog;
}

export interface Form1626asUploadLogRow {
  id: number;
  uploadedAt: string;
  uploadedBy: string;
  uploadedByEmail?: string | null;
  uploadedByDisplayName?: string | null;
  fileName?: string | null;
  recordsImported: number;
  errorsCount: number;
}

/** List 26AS upload history (most recent first) for management section. */
export async function list26asUploadHistory(
  limit: number = 50,
  offset: number = 0
): Promise<{ rows: Form1626asUploadLogRow[]; total: number }> {
  const { rows, count } = await Form1626asUploadLog.findAndCountAll({
    limit,
    offset,
    order: [['uploadedAt', 'DESC']],
    include: [{ model: User, as: 'uploadedByUser', attributes: ['email', 'displayName'], required: false }],
    distinct: true,
  });
  const mapped = rows.map((r) => {
    const u = r as any;
    return {
      id: u.id,
      uploadedAt: (u.uploadedAt instanceof Date ? u.uploadedAt.toISOString() : u.uploadedAt) as string,
      uploadedBy: u.uploadedBy,
      uploadedByEmail: u.uploadedByUser?.email ?? null,
      uploadedByDisplayName: u.uploadedByUser?.displayName ?? null,
      fileName: u.fileName ?? null,
      recordsImported: u.recordsImported ?? 0,
      errorsCount: u.errorsCount ?? 0,
    };
  });
  return { rows: mapped, total: count };
}

export interface Form16DashboardKpi {
  collectionPct: number;
  pendingPct: number;
  submittedPct: number;
  submissionPendingPct: number;
}

export interface Form16DashboardOverall {
  totalAmount: number;
  submittedAmount: number;
  pendingAmount: number;
  totalDealers: number;
  submittedDealerCount: number;
  pendingDealerCount: number;
}

export interface Form16DashboardBreakdownRow {
  label: string;
  totalAmount: number;
  dealerCount: number;
  submittedAmount: number;
  submittedDealerCount: number;
  pendingAmount: number;
  pendingDealerCount: number;
}

export interface Form16DashboardData {
  kpi: Form16DashboardKpi;
  overall: Form16DashboardOverall;
  yearWise: Form16DashboardBreakdownRow[];
  zoneWise: Form16DashboardBreakdownRow[];
}

/**
 * Form16A dashboard for RE users.
 * Uses real DB data:
 * - dealer universe from active dealers
 * - latest submission per dealer+FY+quarter
 * - submitted/credited via form_16_credit_notes
 * Zone mapping follows dealer region code prefix: N* -> North, S* -> South, E* -> East, W* -> West, C* -> Central.
 */
export async function getForm16DashboardData(): Promise<Form16DashboardData> {
  const toNum = (v: unknown): number => {
    const n = Number(v ?? 0);
    return Number.isFinite(n) ? n : 0;
  };

  const [overallRow] = await sequelize.query<{
    total_amount: number | string | null;
    submitted_amount: number | string | null;
    total_dealers: number | string | null;
    submitted_dealer_count: number | string | null;
  }>(
    `
    WITH active_dealers AS (
      SELECT DISTINCT
        TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) AS dealer_code
      FROM dealers d
      WHERE d.is_active = true
        AND TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) <> ''
    ),
    latest_submissions AS (
      SELECT
        s.id,
        s.dealer_code,
        s.financial_year,
        s.quarter,
        COALESCE(s.total_amount, 0)::numeric AS total_amount,
        ROW_NUMBER() OVER (
          PARTITION BY s.dealer_code, s.financial_year, s.quarter
          ORDER BY COALESCE(s.version, 1) DESC, COALESCE(s.submitted_date, s.created_at) DESC, s.id DESC
        ) AS rn
      FROM form16a_submissions s
      INNER JOIN active_dealers ad ON ad.dealer_code = s.dealer_code
    ),
    latest_base AS (
      SELECT id, dealer_code, financial_year, quarter, total_amount
      FROM latest_submissions
      WHERE rn = 1
    ),
    submitted_by_dealer AS (
      SELECT
        lb.dealer_code,
        SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount
      FROM latest_base lb
      LEFT JOIN form_16_credit_notes cn ON cn.submission_id = lb.id
      GROUP BY lb.dealer_code
    )
    SELECT
      COALESCE((SELECT SUM(lb.total_amount) FROM latest_base lb), 0) AS total_amount,
      COALESCE((SELECT SUM(sbd.submitted_amount) FROM submitted_by_dealer sbd), 0) AS submitted_amount,
      COALESCE((SELECT COUNT(*) FROM active_dealers), 0) AS total_dealers,
      COALESCE((
        SELECT COUNT(DISTINCT sbd.dealer_code)
        FROM submitted_by_dealer sbd
        WHERE sbd.submitted_amount > 0
      ), 0) AS submitted_dealer_count
    `,
    { type: QueryTypes.SELECT }
  );

  const totalAmount = toNum(overallRow?.total_amount);
  const submittedAmount = toNum(overallRow?.submitted_amount);
  const totalDealers = Math.max(0, Math.trunc(toNum(overallRow?.total_dealers)));
  const submittedDealerCount = Math.max(0, Math.trunc(toNum(overallRow?.submitted_dealer_count)));
  const pendingDealerCount = Math.max(0, totalDealers - submittedDealerCount);
  const pendingAmount = Math.max(0, totalAmount - submittedAmount);

  const toPct = (part: number, whole: number): number => {
    if (!whole || whole <= 0) return 0;
    return Math.max(0, Math.min(100, Math.round((part / whole) * 100)));
  };

  const yearRowsRaw = await sequelize.query<{
    label: string;
    total_amount: number | string | null;
    dealer_count: number | string | null;
    submitted_amount: number | string | null;
    submitted_dealer_count: number | string | null;
  }>(
    `
    WITH active_dealers AS (
      SELECT DISTINCT
        TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) AS dealer_code
      FROM dealers d
      WHERE d.is_active = true
        AND TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) <> ''
    ),
    latest_submissions AS (
      SELECT
        s.id,
        s.dealer_code,
        s.financial_year,
        s.quarter,
        COALESCE(s.total_amount, 0)::numeric AS total_amount,
        ROW_NUMBER() OVER (
          PARTITION BY s.dealer_code, s.financial_year, s.quarter
          ORDER BY COALESCE(s.version, 1) DESC, COALESCE(s.submitted_date, s.created_at) DESC, s.id DESC
        ) AS rn
      FROM form16a_submissions s
      INNER JOIN active_dealers ad ON ad.dealer_code = s.dealer_code
    ),
    latest_base AS (
      SELECT id, dealer_code, financial_year, quarter, total_amount
      FROM latest_submissions
      WHERE rn = 1
    ),
    by_year AS (
      SELECT
        lb.financial_year AS label,
        SUM(lb.total_amount)::numeric AS total_amount,
        COUNT(DISTINCT lb.dealer_code) AS dealer_count,
        SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount,
        COUNT(DISTINCT CASE WHEN COALESCE(cn.amount, 0) > 0 THEN lb.dealer_code END) AS submitted_dealer_count
      FROM latest_base lb
      LEFT JOIN form_16_credit_notes cn ON cn.submission_id = lb.id
      GROUP BY lb.financial_year
    )
    SELECT * FROM by_year
    ORDER BY label DESC
    `,
    { type: QueryTypes.SELECT }
  );

  const zoneRowsRaw = await sequelize.query<{
    label: string;
    total_amount: number | string | null;
    dealer_count: number | string | null;
    submitted_amount: number | string | null;
    submitted_dealer_count: number | string | null;
  }>(
    `
    WITH active_dealers AS (
      SELECT DISTINCT
        TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) AS dealer_code,
        CASE
          WHEN UPPER(COALESCE(d.region, '')) LIKE 'N%' THEN 'North'
          WHEN UPPER(COALESCE(d.region, '')) LIKE 'S%' THEN 'South'
          WHEN UPPER(COALESCE(d.region, '')) LIKE 'E%' THEN 'East'
          WHEN UPPER(COALESCE(d.region, '')) LIKE 'W%' THEN 'West'
          WHEN UPPER(COALESCE(d.region, '')) LIKE 'C%' THEN 'Central'
          ELSE 'Unknown'
        END AS zone
      FROM dealers d
      WHERE d.is_active = true
        AND TRIM(COALESCE(NULLIF(d.sales_code, ''), NULLIF(d.dlrcode, ''))) <> ''
    ),
    latest_submissions AS (
      SELECT
        s.id,
        s.dealer_code,
        COALESCE(s.total_amount, 0)::numeric AS total_amount,
        ROW_NUMBER() OVER (
          PARTITION BY s.dealer_code, s.financial_year, s.quarter
          ORDER BY COALESCE(s.version, 1) DESC, COALESCE(s.submitted_date, s.created_at) DESC, s.id DESC
        ) AS rn
      FROM form16a_submissions s
      INNER JOIN active_dealers ad ON ad.dealer_code = s.dealer_code
    ),
    latest_base AS (
      SELECT id, dealer_code, total_amount
      FROM latest_submissions
      WHERE rn = 1
    ),
    by_zone AS (
      SELECT
        ad.zone AS label,
        SUM(COALESCE(lb.total_amount, 0))::numeric AS total_amount,
        COUNT(DISTINCT ad.dealer_code) AS dealer_count,
        SUM(COALESCE(cn.amount, 0))::numeric AS submitted_amount,
        COUNT(DISTINCT CASE WHEN COALESCE(cn.amount, 0) > 0 THEN ad.dealer_code END) AS submitted_dealer_count
      FROM active_dealers ad
      LEFT JOIN latest_base lb ON lb.dealer_code = ad.dealer_code
      LEFT JOIN form_16_credit_notes cn ON cn.submission_id = lb.id
      GROUP BY ad.zone
    )
    SELECT * FROM by_zone
    ORDER BY CASE label
      WHEN 'North' THEN 1
      WHEN 'Central' THEN 2
      WHEN 'West' THEN 3
      WHEN 'East' THEN 4
      WHEN 'South' THEN 5
      ELSE 99
    END, label
    `,
    { type: QueryTypes.SELECT }
  );

  const yearWise = (yearRowsRaw || []).map((r) => {
    const totalAmountRow = toNum(r.total_amount);
    const submittedAmountRow = toNum(r.submitted_amount);
    const dealerCountRow = Math.max(0, Math.trunc(toNum(r.dealer_count)));
    const submittedDealerCountRow = Math.max(0, Math.trunc(toNum(r.submitted_dealer_count)));
    return {
      label: r.label,
      totalAmount: totalAmountRow,
      dealerCount: dealerCountRow,
      submittedAmount: submittedAmountRow,
      submittedDealerCount: submittedDealerCountRow,
      pendingAmount: Math.max(0, totalAmountRow - submittedAmountRow),
      pendingDealerCount: Math.max(0, dealerCountRow - submittedDealerCountRow),
    };
  });

  const zoneWise = (zoneRowsRaw || []).map((r) => {
    const totalAmountRow = toNum(r.total_amount);
    const submittedAmountRow = toNum(r.submitted_amount);
    const dealerCountRow = Math.max(0, Math.trunc(toNum(r.dealer_count)));
    const submittedDealerCountRow = Math.max(0, Math.trunc(toNum(r.submitted_dealer_count)));
    return {
      label: r.label,
      totalAmount: totalAmountRow,
      dealerCount: dealerCountRow,
      submittedAmount: submittedAmountRow,
      submittedDealerCount: submittedDealerCountRow,
      pendingAmount: Math.max(0, totalAmountRow - submittedAmountRow),
      pendingDealerCount: Math.max(0, dealerCountRow - submittedDealerCountRow),
    };
  });

  return {
    kpi: {
      collectionPct: toPct(submittedDealerCount, totalDealers),
      pendingPct: toPct(pendingDealerCount, totalDealers),
      submittedPct: toPct(submittedAmount, totalAmount),
      submissionPendingPct: toPct(pendingAmount, totalAmount),
    },
    overall: {
      totalAmount,
      submittedAmount,
      pendingAmount,
      totalDealers,
      submittedDealerCount,
      pendingDealerCount,
    },
    yearWise,
    zoneWise,
  };
}
