import { pool } from "../db/pool.js";
import { ApiError } from "../utils/ApiError.js";
import { logAudit } from "../utils/auditLog.js";
import { formatDateOnly } from "../utils/accountingPeriods.js";
import { createCompanyNotifications, emitNotifications } from "../utils/notifications.js";

const MONTH_LABELS_FR = [
  "Janvier",
  "Février",
  "Mars",
  "Avril",
  "Mai",
  "Juin",
  "Juillet",
  "Août",
  "Septembre",
  "Octobre",
  "Novembre",
  "Décembre",
];

const CLOSURE_MANAGER_ROLES = new Set(["super_admin", "manager", "chief_accountant"]);
const REOPEN_ROLES = new Set(["super_admin", "manager", "chief_accountant"]);

const cleanString = (value) => String(value ?? "").trim();
const roundAmount = (value) => Number(Number(value || 0).toFixed(2));

const getCompanyId = (req, source = req.query) => {
  const companyId = Number(source.company_id || req.user?.companyId || 0);

  if (!companyId) {
    throw new ApiError(400, "company_id is required.");
  }

  return companyId;
};

const ensureRole = (role, allowedRoles, message) => {
  if (!allowedRoles.has(role)) {
    throw new ApiError(403, message);
  }
};

const getPeriodLabel = (period) =>
  `${MONTH_LABELS_FR[Number(period.month) - 1] || `M${period.month}`} ${period.year}`;

const getPeriodEndDate = (period) =>
  new Date(Date.UTC(Number(period.year), Number(period.month), 0))
    .toISOString()
    .slice(0, 10);

const fetchPeriodById = async (db, companyId, periodId) => {
  const { rows } = await db.query(
    `
      SELECT
        ap.id,
        ap.company_id,
        ap.fiscal_year_id,
        ap.month,
        ap.year,
        ap.is_closed,
        ap.closed_at,
        ap.closed_by,
        fy.label AS fiscal_year_label,
        TO_CHAR(fy.start_date, 'YYYY-MM-DD') AS fiscal_year_start_date,
        TO_CHAR(fy.end_date, 'YYYY-MM-DD') AS fiscal_year_end_date,
        fy.is_closed AS fiscal_year_is_closed
      FROM accounting_periods ap
      JOIN fiscal_years fy
        ON fy.id = ap.fiscal_year_id
      WHERE ap.id = $1
        AND ap.company_id = $2
      LIMIT 1
    `,
    [periodId, companyId],
  );

  return rows[0] || null;
};

const fetchFiscalYearById = async (db, companyId, fiscalYearId) => {
  const { rows } = await db.query(
    `
      SELECT
        id,
        company_id,
        label,
        TO_CHAR(start_date, 'YYYY-MM-DD') AS start_date,
        TO_CHAR(end_date, 'YYYY-MM-DD') AS end_date,
        is_closed,
        is_current
      FROM fiscal_years
      WHERE id = $1
        AND company_id = $2
      LIMIT 1
    `,
    [fiscalYearId, companyId],
  );

  return rows[0] || null;
};

const fetchPeriodsForFiscalYear = async (db, companyId, fiscalYearId) => {
  const { rows } = await db.query(
    `
      SELECT
        id,
        fiscal_year_id,
        month,
        year,
        is_closed,
        closed_at,
        closed_by
      FROM accounting_periods
      WHERE company_id = $1
        AND fiscal_year_id = $2
      ORDER BY year ASC, month ASC
    `,
    [companyId, fiscalYearId],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    fiscal_year_id: Number(row.fiscal_year_id),
    month: Number(row.month),
    year: Number(row.year),
    month_label: MONTH_LABELS_FR[Number(row.month) - 1],
    is_closed: row.is_closed,
    closed_at: row.closed_at ? row.closed_at.toISOString() : null,
    closed_by: row.closed_by ? Number(row.closed_by) : null,
  }));
};

const fetchDraftEntriesByPeriod = async (db, periodId) => {
  const { rows } = await db.query(
    `
      SELECT
        je.id,
        je.entry_number,
        TO_CHAR(je.entry_date, 'YYYY-MM-DD') AS entry_date,
        je.label,
        je.piece_ref,
        j.code AS journal_code,
        j.label AS journal_label
      FROM journal_entries je
      JOIN journals j
        ON j.id = je.journal_id
      WHERE je.period_id = $1
        AND je.status = 'draft'
      ORDER BY je.entry_date DESC, je.id DESC
      LIMIT 20
    `,
    [periodId],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    entry_number: row.entry_number,
    entry_date: row.entry_date,
    label: row.label,
    piece_ref: row.piece_ref,
    journal_code: row.journal_code,
    journal_label: row.journal_label,
  }));
};

const fetchDraftEntriesByFiscalYear = async (db, fiscalYearId) => {
  const { rows } = await db.query(
    `
      SELECT
        je.id,
        je.entry_number,
        TO_CHAR(je.entry_date, 'YYYY-MM-DD') AS entry_date,
        je.label,
        je.piece_ref,
        je.period_id,
        ap.month,
        ap.year,
        j.code AS journal_code,
        j.label AS journal_label,
        j.journal_type
      FROM journal_entries je
      JOIN journals j
        ON j.id = je.journal_id
      LEFT JOIN accounting_periods ap
        ON ap.id = je.period_id
      WHERE je.fiscal_year_id = $1
        AND je.status = 'draft'
      ORDER BY je.entry_date DESC, je.id DESC
      LIMIT 50
    `,
    [fiscalYearId],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    entry_number: row.entry_number,
    entry_date: row.entry_date,
    label: row.label,
    piece_ref: row.piece_ref,
    period_id: row.period_id ? Number(row.period_id) : null,
    period_label:
      row.month && row.year
        ? `${MONTH_LABELS_FR[Number(row.month) - 1]} ${row.year}`
        : "Hors période",
    journal_code: row.journal_code,
    journal_label: row.journal_label,
    journal_type: row.journal_type,
  }));
};

const fetchLastReconciliationDate = async (db, companyId, maxDate) => {
  const { rows } = await db.query(
    `
      SELECT MAX(reconciliation_date) AS last_date
      FROM reconciliations
      WHERE company_id = $1
        AND status = 'closed'
        AND reconciliation_date <= $2::date
    `,
    [companyId, maxDate],
  );

  return rows[0]?.last_date ? formatDateOnly(rows[0].last_date) : null;
};

const fetchLetteringProgress = async (db, companyId, dateFrom, dateTo) => {
  const { rows } = await db.query(
    `
      SELECT
        COUNT(*) FILTER (WHERE el.auxiliary_account_id IS NOT NULL)::int AS total_lines,
        COUNT(*) FILTER (
          WHERE el.auxiliary_account_id IS NOT NULL
            AND el.is_lettered = TRUE
        )::int AS lettered_lines
      FROM entry_lines el
      JOIN journal_entries je
        ON je.id = el.entry_id
      WHERE je.company_id = $1
        AND je.status = 'validated'
        AND je.entry_date BETWEEN $2::date AND $3::date
    `,
    [companyId, dateFrom, dateTo],
  );

  const totalLines = Number(rows[0]?.total_lines || 0);
  const letteredLines = Number(rows[0]?.lettered_lines || 0);
  const percent = totalLines ? roundAmount((letteredLines / totalLines) * 100) : 100;

  return {
    total_lines: totalLines,
    lettered_lines: letteredLines,
    percent,
  };
};

const fetchPeriodSummary = async (db, periodId) => {
  const { rows } = await db.query(
    `
      SELECT
        COALESCE(
          SUM(CASE WHEN je.status <> 'cancelled' THEN el.debit ELSE 0 END),
          0
        )::numeric AS total_debit,
        COALESCE(
          SUM(CASE WHEN je.status <> 'cancelled' THEN el.credit ELSE 0 END),
          0
        )::numeric AS total_credit,
        COUNT(DISTINCT je.id) FILTER (WHERE je.status <> 'cancelled')::int AS entries_count,
        COUNT(DISTINCT je.journal_id) FILTER (WHERE je.status <> 'cancelled')::int AS active_journals
      FROM journal_entries je
      LEFT JOIN entry_lines el
        ON el.entry_id = je.id
      WHERE je.period_id = $1
    `,
    [periodId],
  );

  return {
    total_debit: roundAmount(rows[0]?.total_debit || 0),
    total_credit: roundAmount(rows[0]?.total_credit || 0),
    entries_count: Number(rows[0]?.entries_count || 0),
    active_journals: Number(rows[0]?.active_journals || 0),
  };
};

const fetchFiscalYearSummary = async (db, fiscalYearId) => {
  const { rows } = await db.query(
    `
      SELECT
        COALESCE(
          SUM(CASE WHEN je.status <> 'cancelled' THEN el.debit ELSE 0 END),
          0
        )::numeric AS total_debit,
        COALESCE(
          SUM(CASE WHEN je.status <> 'cancelled' THEN el.credit ELSE 0 END),
          0
        )::numeric AS total_credit,
        COUNT(DISTINCT je.id) FILTER (WHERE je.status <> 'cancelled')::int AS entries_count,
        COUNT(DISTINCT je.journal_id) FILTER (WHERE je.status <> 'cancelled')::int AS active_journals
      FROM journal_entries je
      LEFT JOIN entry_lines el
        ON el.entry_id = je.id
      WHERE je.fiscal_year_id = $1
    `,
    [fiscalYearId],
  );

  return {
    total_debit: roundAmount(rows[0]?.total_debit || 0),
    total_credit: roundAmount(rows[0]?.total_credit || 0),
    entries_count: Number(rows[0]?.entries_count || 0),
    active_journals: Number(rows[0]?.active_journals || 0),
  };
};

const fetchProfitAndLossSummary = async (db, companyId, fiscalYear) => {
  const { rows } = await db.query(
    `
      SELECT
        COALESCE(
          SUM(
            CASE
              WHEN a.code LIKE '7%' THEN el.credit - el.debit
              ELSE 0
            END
          ),
          0
        )::numeric AS total_products,
        COALESCE(
          SUM(
            CASE
              WHEN a.code LIKE '6%' THEN el.debit - el.credit
              ELSE 0
            END
          ),
          0
        )::numeric AS total_charges
      FROM entry_lines el
      JOIN journal_entries je
        ON je.id = el.entry_id
      JOIN accounts a
        ON a.id = el.account_id
      WHERE je.company_id = $1
        AND je.status = 'validated'
        AND je.entry_date BETWEEN $2::date AND $3::date
        AND (a.code LIKE '6%' OR a.code LIKE '7%')
    `,
    [companyId, fiscalYear.start_date, fiscalYear.end_date],
  );

  const totalProducts = roundAmount(rows[0]?.total_products || 0);
  const totalCharges = roundAmount(rows[0]?.total_charges || 0);

  return {
    total_products: totalProducts,
    total_charges: totalCharges,
    net_result: roundAmount(totalProducts - totalCharges),
  };
};

const fetchRetainedEarningsBalance = async (db, companyId, fiscalYear) => {
  const { rows } = await db.query(
    `
      SELECT
        COALESCE(
          SUM(el.credit - el.debit),
          0
        )::numeric AS balance
      FROM entry_lines el
      JOIN journal_entries je
        ON je.id = el.entry_id
      JOIN accounts a
        ON a.id = el.account_id
      WHERE je.company_id = $1
        AND je.status = 'validated'
        AND je.entry_date <= $2::date
        AND a.code LIKE '11%'
    `,
    [companyId, fiscalYear.end_date],
  );

  return roundAmount(rows[0]?.balance || 0);
};

const fetchInventoryAdjustments = async (db, fiscalYearId) => {
  const { rows } = await db.query(
    `
      SELECT
        je.id,
        je.entry_number,
        TO_CHAR(je.entry_date, 'YYYY-MM-DD') AS entry_date,
        je.label,
        je.piece_ref,
        j.code AS journal_code,
        j.label AS journal_label,
        j.journal_type
      FROM journal_entries je
      JOIN journals j
        ON j.id = je.journal_id
      WHERE je.fiscal_year_id = $1
        AND j.journal_type IN ('misc', 'assets')
        AND je.status = 'draft'
      ORDER BY je.entry_date DESC, je.id DESC
      LIMIT 20
    `,
    [fiscalYearId],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    entry_number: row.entry_number,
    entry_date: row.entry_date,
    label: row.label,
    piece_ref: row.piece_ref,
    journal_code: row.journal_code,
    journal_label: row.journal_label,
    journal_type: row.journal_type,
  }));
};

const refreshFiscalYearClosedStatus = async (db, fiscalYearId) => {
  const { rows } = await db.query(
    `
      UPDATE fiscal_years fy
      SET is_closed = NOT EXISTS (
        SELECT 1
        FROM accounting_periods ap
        WHERE ap.fiscal_year_id = fy.id
          AND ap.is_closed = FALSE
      )
      WHERE fy.id = $1
      RETURNING id, label, is_closed
    `,
    [fiscalYearId],
  );

  return rows[0] || null;
};

const fetchUserNameById = async (db, userId) => {
  if (!userId) {
    return null;
  }

  const { rows } = await db.query(
    `
      SELECT full_name
      FROM users
      WHERE id = $1
      LIMIT 1
    `,
    [userId],
  );

  return rows[0]?.full_name || null;
};

const findClosureJournal = async (db, companyId) => {
  const existingJournalQuery = await db.query(
    `
      SELECT id, code, label, numbering_mode, journal_type
      FROM journals
      WHERE company_id = $1
        AND is_active = TRUE
        AND (journal_type = 'misc' OR UPPER(code) = 'OD')
      ORDER BY CASE WHEN UPPER(code) = 'OD' THEN 0 ELSE 1 END, id ASC
      LIMIT 1
    `,
    [companyId],
  );

  if (existingJournalQuery.rows[0]) {
    return {
      id: Number(existingJournalQuery.rows[0].id),
      code: existingJournalQuery.rows[0].code,
      label: existingJournalQuery.rows[0].label,
      numbering_mode: existingJournalQuery.rows[0].numbering_mode,
      journal_type: existingJournalQuery.rows[0].journal_type,
    };
  }

  return null;
};

const ensureClosureJournal = async (db, companyId) => {
  const existingJournal = await findClosureJournal(db, companyId);

  if (existingJournal) {
    return existingJournal;
  }

  const { rows } = await db.query(
    `
      INSERT INTO journals (
        company_id,
        code,
        label,
        journal_type,
        default_counterpart_account,
        numbering_mode,
        is_active
      )
      VALUES ($1, 'OD', 'Opérations diverses', 'misc', NULL, 'continuous', TRUE)
      RETURNING id, code, label, numbering_mode, journal_type
    `,
    [companyId],
  );

  return {
    id: Number(rows[0].id),
    code: rows[0].code,
    label: rows[0].label,
    numbering_mode: rows[0].numbering_mode,
    journal_type: rows[0].journal_type,
  };
};

const fetchClosureAccounts = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT id, code, label, balance_normal_side
      FROM accounts
      WHERE company_id = $1
        AND is_active = TRUE
        AND (code LIKE '11%' OR code LIKE '12%')
      ORDER BY
        CASE
          WHEN code = '11' THEN 0
          WHEN code LIKE '11%' THEN 1
          WHEN code = '12' THEN 2
          ELSE 3
        END,
        LENGTH(code) ASC,
        code ASC
    `,
    [companyId],
  );

  const retained = rows.find((row) => row.code === "11") || rows.find((row) => row.code.startsWith("11"));
  const result = rows.find((row) => row.code === "12") || rows.find((row) => row.code.startsWith("12"));

  if (!retained || !result) {
    throw new ApiError(
      400,
      "Les comptes SCF de report à nouveau (11) et de résultat (12) sont requis pour la clôture annuelle.",
    );
  }

  return {
    retained_earnings_account: {
      id: Number(retained.id),
      code: retained.code,
      label: retained.label,
      balance_normal_side: retained.balance_normal_side,
    },
    result_account: {
      id: Number(result.id),
      code: result.code,
      label: result.label,
      balance_normal_side: result.balance_normal_side,
    },
  };
};

const buildEntryNumber = async (db, { companyId, journal, fiscalYear }) => {
  const fiscalSegment =
    (cleanString(fiscalYear.label).replace(/[^0-9A-Za-z]+/g, "") || String(fiscalYear.id)).toUpperCase();
  const prefix = `${cleanString(journal.code).toUpperCase()}-${fiscalSegment}-`;

  const { rows } = await db.query(
    `
      SELECT
        COALESCE(
          MAX(
            NULLIF(SUBSTRING(entry_number FROM '([0-9]+)$'), '')::int
          ),
          0
        ) + 1 AS next_sequence
      FROM journal_entries
      WHERE company_id = $1
        AND journal_id = $2
        AND fiscal_year_id = $3
        AND UPPER(entry_number) LIKE UPPER($4)
    `,
    [companyId, journal.id, fiscalYear.id, `${prefix}%`],
  );

  const nextSequence = Number(rows[0]?.next_sequence || 1);

  return `${prefix}${String(nextSequence).padStart(5, "0")}`;
};

const buildClosureEntryPreview = ({
  journal,
  fiscalYear,
  period,
  resultSummary,
  resultAccount,
  retainedEarningsAccount,
}) => {
  const netResult = roundAmount(resultSummary.net_result);
  const isProfit = netResult >= 0;
  const absoluteResult = Math.abs(netResult);
  const requiresEntry = absoluteResult >= 0.01;

  return {
    journal: {
      id: journal.id,
      code: journal.code,
      label: journal.label,
    },
    entry_date: fiscalYear.end_date,
    period: period
      ? {
          id: period.id,
          label: getPeriodLabel(period),
        }
      : null,
    label: `Clôture annuelle ${fiscalYear.label}`,
    piece_ref: `ANNUAL-CLOSE-${fiscalYear.id}`,
    requires_entry: requiresEntry,
    lines: requiresEntry
      ? [
          {
            account_id: resultAccount.id,
            account_code: resultAccount.code,
            account_label: resultAccount.label,
            debit: isProfit ? absoluteResult : 0,
            credit: isProfit ? 0 : absoluteResult,
          },
          {
            account_id: retainedEarningsAccount.id,
            account_code: retainedEarningsAccount.code,
            account_label: retainedEarningsAccount.label,
            debit: isProfit ? 0 : absoluteResult,
            credit: isProfit ? absoluteResult : 0,
          },
        ]
      : [],
  };
};

const buildChecklistItem = ({ key, label, checked, detail, tone = "success" }) => ({
  key,
  label,
  checked: Boolean(checked),
  detail,
  tone: checked ? "success" : tone,
});

const buildMonthlyPreflightPayload = async (db, companyId, periodId) => {
  const period = await fetchPeriodById(db, companyId, periodId);

  if (!period) {
    throw new ApiError(404, "Période comptable introuvable.");
  }

  const periodLabel = getPeriodLabel(period);
  const draftEntries = await fetchDraftEntriesByPeriod(db, periodId);
  const summary = await fetchPeriodSummary(db, periodId);
  const periodEndDate = getPeriodEndDate(period);
  const lastReconciliationDate = await fetchLastReconciliationDate(
    db,
    companyId,
    periodEndDate,
  );
  const lettering = await fetchLetteringProgress(
    db,
    companyId,
    period.fiscal_year_start_date,
    periodEndDate,
  );

  return {
    mode: "monthly",
    can_close: draftEntries.length === 0 && !period.is_closed,
    period: {
      id: Number(period.id),
      fiscal_year_id: Number(period.fiscal_year_id),
      fiscal_year_label: period.fiscal_year_label,
      month: Number(period.month),
      year: Number(period.year),
      label: periodLabel,
      is_closed: period.is_closed,
    },
    checks: [
      buildChecklistItem({
        key: "entries_validated",
        label: "Toutes les écritures de la période sont validées",
        checked: draftEntries.length === 0,
        detail:
          draftEntries.length === 0
            ? `${summary.entries_count} écriture(s) prêtes`
            : `${draftEntries.length} brouillon(s) détecté(s)`,
        tone: draftEntries.length === 0 ? "success" : "warning",
      }),
      buildChecklistItem({
        key: "bank_reconciliation",
        label: "Rapprochement bancaire effectué",
        checked: Boolean(lastReconciliationDate),
        detail: lastReconciliationDate
          ? `Dernier rapprochement clôturé le ${lastReconciliationDate}`
          : "Aucun rapprochement bancaire clôturé trouvé",
        tone: "warning",
      }),
      buildChecklistItem({
        key: "matching",
        label: "Lettrage à jour",
        checked: lettering.percent >= 90,
        detail: `${lettering.percent.toFixed(2)}% des lignes auxiliaires lettrées`,
        tone: lettering.percent >= 90 ? "success" : "warning",
      }),
      buildChecklistItem({
        key: "draft_warning",
        label: "Écritures en attente de validation",
        checked: draftEntries.length === 0,
        detail: `${draftEntries.length} en attente`,
        tone: "warning",
      }),
    ],
    summary,
    draft_entries: draftEntries,
    bank_reconciliation: {
      last_reconciliation_date: lastReconciliationDate,
    },
    matching: lettering,
  };
};

const buildAnnualPreflightPayload = async (db, companyId, fiscalYearId) => {
  const fiscalYear = await fetchFiscalYearById(db, companyId, fiscalYearId);

  if (!fiscalYear) {
    throw new ApiError(404, "Exercice comptable introuvable.");
  }

  const periods = await fetchPeriodsForFiscalYear(db, companyId, fiscalYearId);
  const closedPeriodsCount = periods.filter((period) => period.is_closed).length;
  const draftEntries = await fetchDraftEntriesByFiscalYear(db, fiscalYearId);
  const summary = await fetchFiscalYearSummary(db, fiscalYearId);
  const lastReconciliationDate = await fetchLastReconciliationDate(
    db,
    companyId,
    fiscalYear.end_date,
  );
  const lettering = await fetchLetteringProgress(
    db,
    companyId,
    fiscalYear.start_date,
    fiscalYear.end_date,
  );
  const inventoryAdjustments = await fetchInventoryAdjustments(db, fiscalYearId);
  const resultSummary = await fetchProfitAndLossSummary(db, companyId, fiscalYear);
  const closureAccounts = await fetchClosureAccounts(db, companyId);
  const closureJournal = (await findClosureJournal(db, companyId)) || {
    id: null,
    code: "OD",
    label: "Opérations diverses",
  };
  const lastPeriod = periods[periods.length - 1] || null;
  const retainedOpeningBalance = await fetchRetainedEarningsBalance(db, companyId, fiscalYear);
  const resultEntryPreview = buildClosureEntryPreview({
    journal: closureJournal,
    fiscalYear,
    period: lastPeriod,
    resultSummary,
    resultAccount: closureAccounts.result_account,
    retainedEarningsAccount: closureAccounts.retained_earnings_account,
  });

  return {
    mode: "annual",
    can_close:
      periods.length > 0 &&
      closedPeriodsCount === periods.length &&
      draftEntries.length === 0 &&
      !fiscalYear.is_closed,
    fiscal_year: {
      id: Number(fiscalYear.id),
      label: fiscalYear.label,
      start_date: fiscalYear.start_date,
      end_date: fiscalYear.end_date,
      is_closed: fiscalYear.is_closed,
      is_current: fiscalYear.is_current,
    },
    checks: [
      buildChecklistItem({
        key: "months_closed",
        label: "Tous les mois de l'exercice sont clôturés",
        checked: periods.length > 0 && closedPeriodsCount === periods.length,
        detail: `${closedPeriodsCount}/${periods.length} période(s) clôturée(s)`,
        tone: "warning",
      }),
      buildChecklistItem({
        key: "entries_validated",
        label: "Toutes les écritures de l'exercice sont validées",
        checked: draftEntries.length === 0,
        detail:
          draftEntries.length === 0
            ? `${summary.entries_count} écriture(s) validées`
            : `${draftEntries.length} brouillon(s) détecté(s)`,
        tone: "warning",
      }),
      buildChecklistItem({
        key: "bank_reconciliation",
        label: "Rapprochement bancaire effectué",
        checked: Boolean(lastReconciliationDate),
        detail: lastReconciliationDate
          ? `Dernier rapprochement clôturé le ${lastReconciliationDate}`
          : "Aucun rapprochement clôturé trouvé",
        tone: "warning",
      }),
      buildChecklistItem({
        key: "matching",
        label: "Lettrage à jour",
        checked: lettering.percent >= 90,
        detail: `${lettering.percent.toFixed(2)}% des lignes auxiliaires lettrées`,
        tone: lettering.percent >= 90 ? "success" : "warning",
      }),
    ],
    summary,
    periods,
    draft_entries: draftEntries,
    inventory_adjustments: inventoryAdjustments,
    result_summary: resultSummary,
    result_entry_preview: resultEntryPreview,
    retained_earnings_preview: {
      account: closureAccounts.retained_earnings_account,
      opening_balance: retainedOpeningBalance,
      carried_forward_balance: roundAmount(retainedOpeningBalance + resultSummary.net_result),
    },
    bank_reconciliation: {
      last_reconciliation_date: lastReconciliationDate,
    },
    matching: lettering,
  };
};

const emitFiscalYearClosedIfNeeded = (req, companyId, fiscalYearStatus, fiscalYearLabel) => {
  if (fiscalYearStatus?.is_closed) {
    req.app
      .get("io")
      ?.to(`company:${companyId}`)
      .emit("fiscal_year:closed", {
        fiscalYearId: Number(fiscalYearStatus.id),
        label: fiscalYearLabel || fiscalYearStatus.label,
      });
  }
};

export const closureController = {
  preflight: async (req, res) => {
    const companyId = getCompanyId(req);
    const periodId = Number(req.query.period_id || 0);

    if (!periodId) {
      throw new ApiError(400, "period_id is required.");
    }

    res.json(await buildMonthlyPreflightPayload(pool, companyId, periodId));
  },

  monthly: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    const periodId = Number(req.body.period_id || 0);

    if (!periodId) {
      throw new ApiError(400, "period_id is required.");
    }

    ensureRole(
      req.user?.role,
      CLOSURE_MANAGER_ROLES,
      "Vous n'êtes pas autorisé à clôturer une période.",
    );

    const client = await pool.connect();

    try {
      await client.query("BEGIN");

      const preflight = await buildMonthlyPreflightPayload(client, companyId, periodId);

      if (preflight.period.is_closed) {
        throw new ApiError(409, "Cette période est déjà clôturée.");
      }

      if (preflight.draft_entries.length > 0) {
        throw new ApiError(
          409,
          "La période contient encore des écritures brouillon non validées.",
        );
      }

      const previousState = await fetchPeriodById(client, companyId, periodId);
      const { rows } = await client.query(
        `
          UPDATE accounting_periods
          SET
            is_closed = TRUE,
            closed_at = CURRENT_TIMESTAMP,
            closed_by = $2
          WHERE id = $1
            AND company_id = $3
          RETURNING id, fiscal_year_id, month, year, is_closed, closed_at, closed_by
        `,
        [periodId, req.user?.sub || null, companyId],
      );

      const closedPeriod = rows[0];
      const fiscalYearStatus = await refreshFiscalYearClosedStatus(
        client,
        Number(closedPeriod.fiscal_year_id),
      );
      const closedByName = await fetchUserNameById(client, req.user?.sub || null);

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "closure.monthly",
        entityType: "accounting_period",
        entityId: periodId,
        oldData: {
          is_closed: previousState?.is_closed || false,
          closed_at: previousState?.closed_at || null,
          closed_by: previousState?.closed_by || null,
        },
        newData: {
          is_closed: true,
          closed_at: closedPeriod.closed_at,
          closed_by: closedPeriod.closed_by,
          period_label: preflight.period.label,
          entries_count: preflight.summary.entries_count,
          modified_entries_count: preflight.summary.entries_count,
        },
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      req.app
        .get("io")
        ?.to(`company:${companyId}`)
        .emit("period:closed", {
          periodId,
          label: preflight.period.label,
        });

      const periodNotifications = await createCompanyNotifications(pool, {
        companyId,
        type: "period_closed",
        message: `Période ${preflight.period.label} clôturée`,
        entityId: periodId,
      });

      emitNotifications(req.app.get("io"), periodNotifications);

      emitFiscalYearClosedIfNeeded(
        req,
        companyId,
        fiscalYearStatus,
        preflight.period.fiscal_year_label,
      );

      res.json({
        message: "Période clôturée avec succès.",
        certificate: {
          period_id: periodId,
          period_label: preflight.period.label,
          fiscal_year_label: preflight.period.fiscal_year_label,
          closed_at: closedPeriod.closed_at,
          closed_by: closedByName || req.user?.email || "Utilisateur",
          entry_count: preflight.summary.entries_count,
        },
        summary: preflight.summary,
      });
    } catch (error) {
      await client.query("ROLLBACK");

      if (error instanceof ApiError && error.statusCode === 409) {
        const preflight = await buildMonthlyPreflightPayload(pool, companyId, periodId);

        return res.status(409).json({
          message: error.message,
          preflight,
        });
      }

      throw error;
    } finally {
      client.release();
    }
  },

  annualPreflight: async (req, res) => {
    const companyId = getCompanyId(req);
    const fiscalYearId = Number(req.query.fiscal_year_id || 0);

    if (!fiscalYearId) {
      throw new ApiError(400, "fiscal_year_id is required.");
    }

    res.json(await buildAnnualPreflightPayload(pool, companyId, fiscalYearId));
  },

  annual: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    const fiscalYearId = Number(req.body.fiscal_year_id || 0);

    if (!fiscalYearId) {
      throw new ApiError(400, "fiscal_year_id is required.");
    }

    ensureRole(
      req.user?.role,
      CLOSURE_MANAGER_ROLES,
      "Vous n'êtes pas autorisé à clôturer un exercice.",
    );

    const client = await pool.connect();

    try {
      await client.query("BEGIN");

      const preflight = await buildAnnualPreflightPayload(client, companyId, fiscalYearId);

      if (preflight.fiscal_year.is_closed) {
        throw new ApiError(409, "Cet exercice est déjà clôturé.");
      }

      if (!preflight.can_close) {
        throw new ApiError(
          409,
          "Les prérequis de clôture annuelle ne sont pas satisfaits.",
        );
      }

      const journal = await ensureClosureJournal(client, companyId);
      const fiscalYear = await fetchFiscalYearById(client, companyId, fiscalYearId);
      const periods = await fetchPeriodsForFiscalYear(client, companyId, fiscalYearId);
      const lastPeriod = periods[periods.length - 1];
      const entryPreview = preflight.result_entry_preview;
      let entry = null;

      if (entryPreview.requires_entry) {
        const existingClosureEntry = await client.query(
          `
            SELECT id, entry_number
            FROM journal_entries
            WHERE company_id = $1
              AND fiscal_year_id = $2
              AND piece_ref = $3
            LIMIT 1
          `,
          [companyId, fiscalYearId, `ANNUAL-CLOSE-${fiscalYearId}`],
        );

        if (existingClosureEntry.rows[0]) {
          throw new ApiError(
            409,
            "Une écriture de clôture annuelle existe déjà pour cet exercice.",
          );
        }

        const entryNumber = await buildEntryNumber(client, {
          companyId,
          journal,
          fiscalYear,
        });

        const entryInsert = await client.query(
          `
            INSERT INTO journal_entries (
              company_id,
              journal_id,
              fiscal_year_id,
              period_id,
              entry_number,
              entry_date,
              label,
              status,
              piece_ref,
              created_by,
              validated_by,
              created_at,
              updated_at
            )
            VALUES (
              $1,
              $2,
              $3,
              $4,
              $5,
              $6::date,
              $7,
              'validated',
              $8,
              $9,
              $9,
              CURRENT_TIMESTAMP,
              CURRENT_TIMESTAMP
            )
            RETURNING id, entry_number, entry_date, label, period_id
          `,
          [
            companyId,
            journal.id,
            fiscalYearId,
            lastPeriod?.id || null,
            entryNumber,
            fiscalYear.end_date,
            entryPreview.label,
            entryPreview.piece_ref,
            req.user?.sub || null,
          ],
        );

        entry = entryInsert.rows[0];

        for (const line of entryPreview.lines) {
          await client.query(
            `
              INSERT INTO entry_lines (
                entry_id,
                account_id,
                auxiliary_account_id,
                label,
                debit,
                credit,
                due_date,
                is_lettered,
                letter_code
              )
              VALUES ($1, $2, NULL, $3, $4, $5, NULL, FALSE, NULL)
            `,
            [
              entry.id,
              line.account_id,
              entryPreview.label,
              line.debit,
              line.credit,
            ],
          );
        }
      }

      const fiscalYearUpdate = await client.query(
        `
          UPDATE fiscal_years
          SET is_closed = TRUE
          WHERE id = $1
            AND company_id = $2
          RETURNING id, label, is_closed
        `,
        [fiscalYearId, companyId],
      );

      const closedByName = await fetchUserNameById(client, req.user?.sub || null);

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "closure.annual",
        entityType: "fiscal_year",
        entityId: fiscalYearId,
        oldData: {
          is_closed: false,
        },
        newData: {
          is_closed: true,
          fiscal_year_label: fiscalYear.label,
          generated_entry_id: entry ? Number(entry.id) : null,
          generated_entry_number: entry?.entry_number || null,
          generated_entry_count: entry ? 1 : 0,
          modified_entries_count: entry ? 1 : 0,
          net_result: preflight.result_summary.net_result,
        },
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      req.app
        .get("io")
        ?.to(`company:${companyId}`)
        .emit("fiscal_year:closed", {
          fiscalYearId,
          label: fiscalYear.label,
        });

      const fiscalYearNotifications = await createCompanyNotifications(pool, {
        companyId,
        type: "fiscal_year_closed",
        message: `Exercice ${fiscalYear.label} clôturé`,
        entityId: fiscalYearId,
      });

      emitNotifications(req.app.get("io"), fiscalYearNotifications);

      res.json({
        message: "Exercice clôturé avec succès.",
        certificate: {
          fiscal_year_id: fiscalYearId,
          fiscal_year_label: fiscalYear.label,
          closed_at: new Date().toISOString(),
          closed_by: closedByName || req.user?.email || "Utilisateur",
          generated_entry_id: entry ? Number(entry.id) : null,
          generated_entry_number: entry?.entry_number || null,
          net_result: preflight.result_summary.net_result,
          is_closed: fiscalYearUpdate.rows[0]?.is_closed || true,
        },
        result_summary: preflight.result_summary,
        result_entry: entry
          ? {
              id: Number(entry.id),
              entry_number: entry.entry_number,
              label: entry.label,
              entry_date: formatDateOnly(entry.entry_date),
              journal_code: journal.code,
              journal_label: journal.label,
            }
          : null,
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

  reopen: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    const periodId = Number(req.body.period_id || 0);
    const justification = cleanString(req.body.justification);

    if (!periodId) {
      throw new ApiError(400, "period_id is required.");
    }

    if (!justification) {
      throw new ApiError(400, "La justification de réouverture est obligatoire.");
    }

    ensureRole(
      req.user?.role,
      REOPEN_ROLES,
      "La réouverture exceptionnelle est réservée à l'administration et au chef comptable.",
    );

    const client = await pool.connect();

    try {
      await client.query("BEGIN");

      const period = await fetchPeriodById(client, companyId, periodId);

      if (!period) {
        throw new ApiError(404, "Période comptable introuvable.");
      }

      const { rows } = await client.query(
        `
          UPDATE accounting_periods
          SET
            is_closed = FALSE,
            closed_at = NULL,
            closed_by = NULL
          WHERE id = $1
            AND company_id = $2
          RETURNING id, fiscal_year_id, month, year, is_closed, closed_at, closed_by
        `,
        [periodId, companyId],
      );

      await client.query(
        `
          UPDATE fiscal_years
          SET is_closed = FALSE
          WHERE id = $1
        `,
        [period.fiscal_year_id],
      );

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "closure.reopen",
        entityType: "accounting_period",
        entityId: periodId,
        oldData: {
          is_closed: period.is_closed,
          closed_at: period.closed_at,
          closed_by: period.closed_by,
        },
        newData: {
          is_closed: false,
          closed_at: rows[0].closed_at,
          closed_by: rows[0].closed_by,
          period_label: getPeriodLabel(rows[0]),
          justification,
          modified_entries_count: 0,
        },
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.json({
        message: "Période réouverte avec succès.",
        item: {
          id: Number(rows[0].id),
          fiscal_year_id: Number(rows[0].fiscal_year_id),
          label: getPeriodLabel(rows[0]),
          is_closed: rows[0].is_closed,
        },
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

  auditLog: async (req, res) => {
    const companyId = getCompanyId(req);

    const { rows } = await pool.query(
      `
        SELECT
          al.id,
          al.action,
          al.entity_type,
          al.entity_id,
          al.old_data,
          al.new_data,
          al.created_at,
          u.full_name AS user_name,
          ap.month AS period_month,
          ap.year AS period_year,
          fy.label AS fiscal_year_label
        FROM audit_log al
        LEFT JOIN users u
          ON u.id = al.user_id
        LEFT JOIN accounting_periods ap
          ON al.entity_type = 'accounting_period'
         AND ap.id = al.entity_id
        LEFT JOIN fiscal_years fy
          ON (
            (al.entity_type = 'fiscal_year' AND fy.id = al.entity_id)
            OR ap.fiscal_year_id = fy.id
          )
        WHERE al.company_id = $1
          AND al.action IN (
            'closure.monthly',
            'closure.annual',
            'closure.reopen',
            'periods.close',
            'periods.reopen'
          )
        ORDER BY al.created_at DESC, al.id DESC
      `,
      [companyId],
    );

    res.json({
      company_id: companyId,
      items: rows.map((row) => {
        const newData = row.new_data || {};
        const oldData = row.old_data || {};
        const derivedPeriodLabel =
          newData.period_label ||
          oldData.period_label ||
          (row.period_month && row.period_year
            ? `${MONTH_LABELS_FR[Number(row.period_month) - 1]} ${row.period_year}`
            : row.fiscal_year_label || "—");

        return {
          id: Number(row.id),
          action: row.action,
          action_label:
            row.action === "closure.monthly"
              ? "Clôture mensuelle"
              : row.action === "closure.annual"
                ? "Clôture annuelle"
                : row.action === "closure.reopen"
                  ? "Réouverture exceptionnelle"
                  : row.action === "periods.close"
                    ? "Clôture de période"
                    : "Réouverture de période",
          period_label: derivedPeriodLabel,
          fiscal_year_label:
            newData.fiscal_year_label || oldData.fiscal_year_label || row.fiscal_year_label || "—",
          justification: newData.justification || oldData.justification || "",
          modified_entries_count: Number(
            newData.modified_entries_count ||
              newData.entries_count ||
              newData.generated_entry_count ||
              oldData.modified_entries_count ||
              0,
          ),
          performed_by: row.user_name || "Utilisateur inconnu",
          created_at: row.created_at.toISOString(),
        };
      }),
    });
  },
};
