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

const DEPRECIATION_METHOD_LABELS = {
  linear: "Linéaire",
  degressive: "Dégressif",
};

const ASSET_STATUS_LABELS = {
  active: "Actif",
  sold: "Cédé",
  scrapped: "Mis au rebut",
};

const DISPOSAL_TYPE_LABELS = {
  sale: "Cession",
  scrap: "Mise au rebut",
};

const cleanString = (value) => String(value ?? "").trim();

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 parseCurrency = (value, fieldLabel) => {
  if (value === null || value === undefined || value === "") {
    return 0;
  }

  const normalized = cleanString(value).replace(/\s+/g, "").replace(",", ".");
  const amount = Number.parseFloat(normalized);

  if (!Number.isFinite(amount) || amount < 0) {
    throw new ApiError(400, `${fieldLabel} invalide.`);
  }

  return Number(amount.toFixed(2));
};

const parseInteger = (value, fieldLabel) => {
  const parsed = Number(value);

  if (!Number.isFinite(parsed) || parsed <= 0) {
    throw new ApiError(400, `${fieldLabel} invalide.`);
  }

  return Math.round(parsed);
};

const parseDateInput = (value, fieldLabel) => {
  if (!cleanString(value)) {
    throw new ApiError(400, `${fieldLabel} invalide.`);
  }

  return formatDateOnly(parseFiscalDate(value, fieldLabel));
};

const normalizeDepreciationMethod = (value) => {
  const method = cleanString(value).toLowerCase();

  if (!["linear", "degressive"].includes(method)) {
    throw new ApiError(400, "La méthode d'amortissement est invalide.");
  }

  return method;
};

const normalizeAssetStatus = (value) => {
  const status = cleanString(value || "active").toLowerCase();

  if (!["active", "sold", "scrapped"].includes(status)) {
    throw new ApiError(400, "Le statut d'immobilisation est invalide.");
  }

  return status;
};

const normalizeDisposalType = (value) => {
  const type = cleanString(value).toLowerCase();

  if (!["sale", "scrap"].includes(type)) {
    throw new ApiError(400, "Le type de sortie est invalide.");
  }

  return type;
};

const firstDayOfMonth = (value) => {
  const date = new Date(`${formatDateOnly(value)}T00:00:00Z`);

  return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1));
};

const lastDayOfMonth = (value) => {
  const date = new Date(`${formatDateOnly(value)}T00:00:00Z`);

  return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0));
};

const addMonths = (date, months) =>
  new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + months, 1));

const getMonthKey = (value) => {
  const date = value instanceof Date ? value : new Date(`${formatDateOnly(value)}T00:00:00Z`);

  return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}`;
};

const roundAmount = (value) => Number(toNumber(value).toFixed(2));

const getCategoryPrefixLabel = (code) => {
  if (code.startsWith("28")) {
    return "classe 28";
  }

  if (code.startsWith("68")) {
    return "classe 68";
  }

  return "classe 2";
};

const serializeCategoryForAudit = (category) => ({
  id: Number(category.id),
  code: category.code,
  label: category.label,
  depreciation_method: category.depreciation_method,
  default_duration_years: Number(category.default_duration_years),
  account_asset: category.account_asset,
  account_depreciation: category.account_depreciation,
  account_expense: category.account_expense,
});

const serializeAssetForAudit = (asset) => ({
  id: Number(asset.id),
  code: asset.code,
  label: asset.label,
  category_id: Number(asset.category_id),
  acquisition_date: asset.acquisition_date,
  acquisition_value: roundAmount(asset.acquisition_value),
  residual_value: roundAmount(asset.residual_value),
  duration_years: Number(asset.duration_years),
  depreciation_method: asset.depreciation_method,
  depreciation_start_date: asset.depreciation_start_date,
  assignment_center: asset.assignment_center,
  status: asset.status,
  description: asset.description,
  disposal_date: asset.disposal_date,
  disposal_type: asset.disposal_type,
  disposal_price: roundAmount(asset.disposal_price),
  disposal_gain_loss: roundAmount(asset.disposal_gain_loss),
  disposed_entry_id: asset.disposed_entry_id ? Number(asset.disposed_entry_id) : null,
});

const fetchAccountByCode = async (db, companyId, code) => {
  if (!cleanString(code)) {
    return null;
  }

  const { rows } = await db.query(
    `
      SELECT
        id,
        code,
        label,
        account_type,
        is_movable,
        is_active,
        balance_normal_side
      FROM accounts
      WHERE company_id = $1
        AND code = $2
      LIMIT 1
    `,
    [companyId, code],
  );

  return rows[0]
    ? {
        ...rows[0],
        id: Number(rows[0].id),
      }
    : null;
};

const resolvePostingAccount = async ({
  db,
  companyId,
  code,
  fieldLabel,
  allowedPrefixes,
  disallowedPrefixes = [],
}) => {
  const normalizedCode = cleanString(code);

  if (!normalizedCode) {
    throw new ApiError(400, `${fieldLabel} est obligatoire.`);
  }

  const account = await fetchAccountByCode(db, companyId, normalizedCode);

  if (!account) {
    throw new ApiError(400, `${fieldLabel} introuvable.`);
  }

  if (!allowedPrefixes.some((prefix) => account.code.startsWith(prefix))) {
    throw new ApiError(
      400,
      `${fieldLabel} doit appartenir à ${allowedPrefixes
        .map((prefix) => getCategoryPrefixLabel(prefix))
        .join(" ou ")}.`,
    );
  }

  if (disallowedPrefixes.some((prefix) => account.code.startsWith(prefix))) {
    throw new ApiError(400, `${fieldLabel} ne peut pas utiliser le compte ${account.code}.`);
  }

  if (!account.is_active) {
    throw new ApiError(400, `${fieldLabel} est inactif.`);
  }

  if (!account.is_movable) {
    throw new ApiError(400, `${fieldLabel} doit être un compte mouvementable.`);
  }

  return account;
};

const fetchAssetCategoryById = async (db, companyId, categoryId) => {
  const { rows } = await db.query(
    `
      SELECT
        ac.*,
        asset_account.label AS account_asset_label,
        depreciation_account.label AS account_depreciation_label,
        expense_account.label AS account_expense_label
      FROM asset_categories ac
      LEFT JOIN accounts asset_account
        ON asset_account.company_id = ac.company_id
       AND asset_account.code = ac.account_asset
      LEFT JOIN accounts depreciation_account
        ON depreciation_account.company_id = ac.company_id
       AND depreciation_account.code = ac.account_depreciation
      LEFT JOIN accounts expense_account
        ON expense_account.company_id = ac.company_id
       AND expense_account.code = ac.account_expense
      WHERE ac.company_id = $1
        AND ac.id = $2
      LIMIT 1
    `,
    [companyId, categoryId],
  );

  return rows[0] || null;
};

const fetchAssetCategoryList = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT
        ac.id,
        ac.code,
        ac.label,
        ac.depreciation_method,
        ac.default_duration_years,
        ac.account_asset,
        ac.account_depreciation,
        ac.account_expense,
        asset_account.label AS account_asset_label,
        depreciation_account.label AS account_depreciation_label,
        expense_account.label AS account_expense_label
      FROM asset_categories ac
      LEFT JOIN accounts asset_account
        ON asset_account.company_id = ac.company_id
       AND asset_account.code = ac.account_asset
      LEFT JOIN accounts depreciation_account
        ON depreciation_account.company_id = ac.company_id
       AND depreciation_account.code = ac.account_depreciation
      LEFT JOIN accounts expense_account
        ON expense_account.company_id = ac.company_id
       AND expense_account.code = ac.account_expense
      WHERE ac.company_id = $1
      ORDER BY ac.code ASC
    `,
    [companyId],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    code: row.code,
    label: row.label,
    depreciation_method: row.depreciation_method,
    depreciation_method_label:
      DEPRECIATION_METHOD_LABELS[row.depreciation_method] || row.depreciation_method,
    default_duration_years: Number(row.default_duration_years),
    account_asset: row.account_asset,
    account_asset_label: row.account_asset_label,
    account_depreciation: row.account_depreciation,
    account_depreciation_label: row.account_depreciation_label,
    account_expense: row.account_expense,
    account_expense_label: row.account_expense_label,
  }));
};

const normalizeCategoryPayload = async ({ db, companyId, body, existingCategory = null }) => {
  const code = cleanString(body.code || existingCategory?.code).toUpperCase();
  const label = cleanString(body.label || existingCategory?.label);
  const depreciationMethod = normalizeDepreciationMethod(
    body.depreciation_method || existingCategory?.depreciation_method || "linear",
  );
  const defaultDurationYears = parseInteger(
    body.default_duration_years ?? existingCategory?.default_duration_years,
    "La durée par défaut",
  );
  const accountAsset = await resolvePostingAccount({
    db,
    companyId,
    code: body.account_asset || existingCategory?.account_asset,
    fieldLabel: "Le compte d'immobilisation",
    allowedPrefixes: ["2"],
    disallowedPrefixes: ["28", "29"],
  });
  const accountDepreciation = await resolvePostingAccount({
    db,
    companyId,
    code: body.account_depreciation || existingCategory?.account_depreciation,
    fieldLabel: "Le compte d'amortissement",
    allowedPrefixes: ["28"],
  });
  const accountExpense = await resolvePostingAccount({
    db,
    companyId,
    code: body.account_expense || existingCategory?.account_expense,
    fieldLabel: "Le compte de dotation",
    allowedPrefixes: ["68"],
  });

  if (!code) {
    throw new ApiError(400, "Le code de catégorie est obligatoire.");
  }

  if (!label) {
    throw new ApiError(400, "Le libellé de catégorie est obligatoire.");
  }

  return {
    code,
    label,
    depreciation_method: depreciationMethod,
    default_duration_years: defaultDurationYears,
    account_asset: accountAsset.code,
    account_depreciation: accountDepreciation.code,
    account_expense: accountExpense.code,
  };
};

const buildAssetCode = async (db, companyId, categoryCode) => {
  const prefix = cleanString(categoryCode).replace(/[^A-Z0-9]+/gi, "").toUpperCase() || "IMM";
  const { rows } = await db.query(
    `
      SELECT
        COALESCE(MAX(NULLIF(SUBSTRING(code FROM '([0-9]+)$'), '')::int), 0) + 1 AS next_sequence
      FROM assets
      WHERE company_id = $1
        AND UPPER(code) LIKE UPPER($2)
    `,
    [companyId, `${prefix}-%`],
  );

  return `${prefix}-${String(Number(rows[0]?.next_sequence || 1)).padStart(4, "0")}`;
};

const fetchFiscalYears = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT id, label, start_date, end_date, is_closed, is_current
      FROM fiscal_years
      WHERE company_id = $1
      ORDER BY start_date ASC
    `,
    [companyId],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    label: row.label,
    start_date: formatDateOnly(row.start_date),
    end_date: formatDateOnly(row.end_date),
    is_closed: row.is_closed,
    is_current: row.is_current,
  }));
};

const findFiscalYearForDate = (fiscalYears, dateValue) => {
  const isoDate = formatDateOnly(dateValue);

  return (
    fiscalYears.find(
      (fiscalYear) => isoDate >= fiscalYear.start_date && isoDate <= fiscalYear.end_date,
    ) || null
  );
};

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

  return rows[0]
    ? {
        id: Number(rows[0].id),
        label: rows[0].label,
        start_date: formatDateOnly(rows[0].start_date),
        end_date: formatDateOnly(rows[0].end_date),
        is_closed: rows[0].is_closed,
      }
    : null;
};

const fetchPeriodsForFiscalYear = async (db, companyId, fiscalYearId) => {
  const { rows } = await db.query(
    `
      SELECT
        ap.id,
        ap.fiscal_year_id,
        ap.month,
        ap.year,
        ap.is_closed,
        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.company_id = $1
        AND ap.fiscal_year_id = $2
      ORDER BY ap.year ASC, ap.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),
    label: `${String(row.month).padStart(2, "0")}/${row.year}`,
    is_closed: row.is_closed,
    fiscal_year_is_closed: row.fiscal_year_is_closed,
  }));
};

const fetchPeriodById = async (db, companyId, periodId) => {
  const { rows } = await db.query(
    `
      SELECT
        ap.id,
        ap.fiscal_year_id,
        ap.month,
        ap.year,
        ap.is_closed,
        fy.label AS fiscal_year_label,
        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.company_id = $1
        AND ap.id = $2
      LIMIT 1
    `,
    [companyId, periodId],
  );

  return rows[0]
    ? {
        id: Number(rows[0].id),
        fiscal_year_id: Number(rows[0].fiscal_year_id),
        fiscal_year_label: rows[0].fiscal_year_label,
        month: Number(rows[0].month),
        year: Number(rows[0].year),
        label: `${String(rows[0].month).padStart(2, "0")}/${rows[0].year}`,
        is_closed: rows[0].is_closed,
        fiscal_year_is_closed: rows[0].fiscal_year_is_closed,
      }
    : null;
};

const fetchDepreciationLinesByAssetId = async (db, assetId) => {
  const { rows } = await db.query(
    `
      SELECT
        dl.id,
        dl.asset_id,
        dl.fiscal_year_id,
        TO_CHAR(dl.period_date, 'YYYY-MM-DD') AS period_date,
        dl.amount_depreciation,
        dl.cumulated_depreciation,
        dl.net_book_value,
        dl.entry_id,
        fy.label AS fiscal_year_label
      FROM depreciation_lines dl
      JOIN fiscal_years fy
        ON fy.id = dl.fiscal_year_id
      WHERE dl.asset_id = $1
      ORDER BY dl.period_date ASC, dl.id ASC
    `,
    [assetId],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    asset_id: Number(row.asset_id),
    fiscal_year_id: Number(row.fiscal_year_id),
    fiscal_year_label: row.fiscal_year_label,
    period_date: row.period_date,
    amount_depreciation: roundAmount(row.amount_depreciation),
    cumulated_depreciation: roundAmount(row.cumulated_depreciation),
    net_book_value: roundAmount(row.net_book_value),
    entry_id: row.entry_id ? Number(row.entry_id) : null,
  }));
};

const fetchAssetsSnapshot = async (db, companyId, { assetId = null, filters = {} } = {}) => {
  const values = [companyId];
  const conditions = ["a.company_id = $1"];

  if (assetId) {
    values.push(assetId);
    conditions.push(`a.id = $${values.length}`);
  }

  if (filters.category_id) {
    values.push(Number(filters.category_id));
    conditions.push(`a.category_id = $${values.length}`);
  }

  if (filters.status) {
    values.push(cleanString(filters.status).toLowerCase());
    conditions.push(`a.status = $${values.length}`);
  }

  if (filters.date_from) {
    values.push(parseDateInput(filters.date_from, "Date de début"));
    conditions.push(`a.acquisition_date >= $${values.length}::date`);
  }

  if (filters.date_to) {
    values.push(parseDateInput(filters.date_to, "Date de fin"));
    conditions.push(`a.acquisition_date <= $${values.length}::date`);
  }

  const { rows } = await db.query(
    `
      SELECT
        a.id,
        a.company_id,
        a.category_id,
        a.code,
        a.label,
        TO_CHAR(a.acquisition_date, 'YYYY-MM-DD') AS acquisition_date,
        a.acquisition_value,
        a.residual_value,
        a.duration_years,
        a.depreciation_method,
        a.assignment_center,
        a.status,
        a.description,
        TO_CHAR(COALESCE(a.depreciation_start_date, a.acquisition_date), 'YYYY-MM-DD') AS depreciation_start_date,
        TO_CHAR(a.disposal_date, 'YYYY-MM-DD') AS disposal_date,
        a.disposal_type,
        a.disposal_price,
        a.disposal_gain_loss,
        a.disposed_entry_id,
        c.code AS category_code,
        c.label AS category_label,
        c.account_asset,
        c.account_depreciation,
        c.account_expense,
        COALESCE(dep.total_depreciation, 0)::numeric AS accumulated_depreciation
      FROM assets a
      JOIN asset_categories c
        ON c.id = a.category_id
      LEFT JOIN LATERAL (
        SELECT SUM(dl.amount_depreciation) AS total_depreciation
        FROM depreciation_lines dl
        WHERE dl.asset_id = a.id
      ) dep
        ON TRUE
      WHERE ${conditions.join(" AND ")}
      ORDER BY a.acquisition_date DESC, a.code ASC
    `,
    values,
  );

  return rows.map((row) => {
    const acquisitionValue = roundAmount(row.acquisition_value);
    const accumulatedDepreciation = roundAmount(row.accumulated_depreciation);
    const netBookValue = roundAmount(acquisitionValue - accumulatedDepreciation);
    const residualValue = roundAmount(row.residual_value);
    const depreciationRate = row.duration_years
      ? Number((100 / Number(row.duration_years)).toFixed(2))
      : 0;

    return {
      id: Number(row.id),
      company_id: Number(row.company_id),
      category_id: Number(row.category_id),
      code: row.code,
      label: row.label,
      acquisition_date: row.acquisition_date,
      acquisition_value: acquisitionValue,
      formatted_acquisition_value: formatDzdAmount(acquisitionValue),
      residual_value: residualValue,
      formatted_residual_value: formatDzdAmount(residualValue),
      duration_years: Number(row.duration_years),
      depreciation_method: row.depreciation_method,
      depreciation_method_label:
        DEPRECIATION_METHOD_LABELS[row.depreciation_method] || row.depreciation_method,
      depreciation_rate: depreciationRate,
      assignment_center: row.assignment_center,
      status: row.status,
      status_label: ASSET_STATUS_LABELS[row.status] || row.status,
      description: row.description,
      depreciation_start_date: row.depreciation_start_date,
      disposal_date: row.disposal_date,
      disposal_type: row.disposal_type,
      disposal_type_label: row.disposal_type
        ? DISPOSAL_TYPE_LABELS[row.disposal_type] || row.disposal_type
        : null,
      disposal_price: roundAmount(row.disposal_price),
      formatted_disposal_price: formatDzdAmount(row.disposal_price),
      disposal_gain_loss: roundAmount(row.disposal_gain_loss),
      formatted_disposal_gain_loss: formatDzdAmount(row.disposal_gain_loss),
      disposed_entry_id: row.disposed_entry_id ? Number(row.disposed_entry_id) : null,
      category: {
        id: Number(row.category_id),
        code: row.category_code,
        label: row.category_label,
        account_asset: row.account_asset,
        account_depreciation: row.account_depreciation,
        account_expense: row.account_expense,
      },
      accumulated_depreciation: accumulatedDepreciation,
      formatted_accumulated_depreciation: formatDzdAmount(accumulatedDepreciation),
      net_book_value: netBookValue,
      formatted_net_book_value: formatDzdAmount(netBookValue),
    };
  });
};

const normalizeAssetPayload = async ({ db, companyId, body, existingAsset = null }) => {
  const categoryId = Number(body.category_id || existingAsset?.category_id || 0);

  if (!categoryId) {
    throw new ApiError(400, "La catégorie est obligatoire.");
  }

  const category = await fetchAssetCategoryById(db, companyId, categoryId);

  if (!category) {
    throw new ApiError(400, "La catégorie d'immobilisation est introuvable.");
  }

  const label = cleanString(body.label || existingAsset?.label);

  if (!label) {
    throw new ApiError(400, "Le libellé est obligatoire.");
  }

  const acquisitionDate = parseDateInput(
    body.acquisition_date || existingAsset?.acquisition_date,
    "La date d'acquisition",
  );
  const acquisitionValue = parseCurrency(
    body.acquisition_value ?? existingAsset?.acquisition_value,
    "La valeur d'origine",
  );
  const residualValue = parseCurrency(
    body.residual_value ?? existingAsset?.residual_value,
    "La valeur résiduelle",
  );
  const durationYears = parseInteger(
    body.duration_years ?? category.default_duration_years ?? existingAsset?.duration_years,
    "La durée d'amortissement",
  );
  const depreciationMethod = normalizeDepreciationMethod(
    body.depreciation_method || category.depreciation_method || existingAsset?.depreciation_method,
  );
  const depreciationStartDate = parseDateInput(
    body.depreciation_start_date ||
      existingAsset?.depreciation_start_date ||
      acquisitionDate,
    "La date de début d'amortissement",
  );
  const assignmentCenter = cleanString(body.assignment_center || existingAsset?.assignment_center);
  const description = cleanString(body.description || existingAsset?.description);
  const status = normalizeAssetStatus(body.status || existingAsset?.status || "active");
  let code = cleanString(body.code || existingAsset?.code).toUpperCase();

  if (residualValue > acquisitionValue) {
    throw new ApiError(
      400,
      "La valeur résiduelle ne peut pas dépasser la valeur d'origine.",
    );
  }

  if (depreciationStartDate < acquisitionDate) {
    throw new ApiError(
      400,
      "La date de début d'amortissement ne peut pas être antérieure à l'acquisition.",
    );
  }

  if (!code) {
    code = await buildAssetCode(db, companyId, category.code);
  }

  return {
    code,
    label,
    category_id: Number(category.id),
    acquisition_date: acquisitionDate,
    acquisition_value: acquisitionValue,
    residual_value: residualValue,
    duration_years: durationYears,
    depreciation_method: depreciationMethod,
    assignment_center: assignmentCenter,
    description,
    depreciation_start_date: depreciationStartDate,
    status,
    category: {
      id: Number(category.id),
      code: category.code,
      label: category.label,
      account_asset: category.account_asset,
      account_depreciation: category.account_depreciation,
      account_expense: category.account_expense,
    },
  };
};

const fetchCategoryPostingAccounts = async (db, companyId, category) => {
  const assetAccount = await resolvePostingAccount({
    db,
    companyId,
    code: category.account_asset,
    fieldLabel: "Le compte d'immobilisation de la catégorie",
    allowedPrefixes: ["2"],
  });
  const depreciationAccount = await resolvePostingAccount({
    db,
    companyId,
    code: category.account_depreciation,
    fieldLabel: "Le compte d'amortissement de la catégorie",
    allowedPrefixes: ["28"],
  });
  const expenseAccount = await resolvePostingAccount({
    db,
    companyId,
    code: category.account_expense,
    fieldLabel: "Le compte de dotation de la catégorie",
    allowedPrefixes: ["68"],
  });

  return {
    assetAccount,
    depreciationAccount,
    expenseAccount,
  };
};

const buildAssetSchedule = ({
  asset,
  fiscalYears,
  periodsByMonthKey,
  existingLines = [],
}) => {
  const acquisitionValue = roundAmount(asset.acquisition_value);
  const residualValue = roundAmount(asset.residual_value);
  const startMonth = firstDayOfMonth(asset.depreciation_start_date || asset.acquisition_date);
  const totalMonths = Math.max(1, Number(asset.duration_years) * 12);
  const finalMonth = addMonths(startMonth, totalMonths - 1);
  const disposalMonth = asset.disposal_date ? firstDayOfMonth(asset.disposal_date) : null;
  const lastMonth = disposalMonth && disposalMonth < finalMonth ? disposalMonth : finalMonth;
  const depreciableBase = Math.max(0, roundAmount(acquisitionValue - residualValue));
  const monthlyLinearAmount = totalMonths ? roundAmount(depreciableBase / totalMonths) : 0;
  const annualLinearRate = Number((100 / Math.max(1, Number(asset.duration_years))).toFixed(2));
  const annualDegressiveRate = Number(Math.min(100, annualLinearRate * 1.75).toFixed(2));
  const existingByDate = new Map(existingLines.map((line) => [line.period_date, line]));
  const rows = [];
  let accumulated = 0;
  let netBookValue = acquisitionValue;
  let index = 0;

  while (index < totalMonths) {
    const currentMonth = addMonths(startMonth, index);

    if (currentMonth > lastMonth) {
      break;
    }

    const monthEndDate = lastDayOfMonth(currentMonth);
    const periodDate = formatDateOnly(monthEndDate);
    const existingLine = existingByDate.get(periodDate);
    const remainingDepreciable = Math.max(0, roundAmount(acquisitionValue - residualValue - accumulated));
    let amount = 0;

    if (existingLine) {
      amount = roundAmount(existingLine.amount_depreciation);
      accumulated = roundAmount(existingLine.cumulated_depreciation);
      netBookValue = roundAmount(existingLine.net_book_value);
    } else if (remainingDepreciable > 0) {
      if (asset.depreciation_method === "degressive") {
        const currentBase = Math.max(0, roundAmount(netBookValue - residualValue));
        const monthlyRate = annualDegressiveRate / 100 / 12;
        const remainingMonths = totalMonths - index;
        const calculatedAmount = roundAmount(currentBase * monthlyRate);
        const linearFallback = remainingMonths > 0 ? roundAmount(currentBase / remainingMonths) : currentBase;

        amount = Math.max(calculatedAmount, linearFallback);
      } else {
        amount = monthlyLinearAmount;
      }

      if (index === totalMonths - 1 || amount > remainingDepreciable) {
        amount = remainingDepreciable;
      }

      amount = roundAmount(amount);
      accumulated = roundAmount(accumulated + amount);
      netBookValue = roundAmount(acquisitionValue - accumulated);
    }

    const fiscalYear = findFiscalYearForDate(fiscalYears, periodDate);
    const period = periodsByMonthKey.get(getMonthKey(periodDate)) || null;

    rows.push({
      index: index + 1,
      fiscal_year_id: fiscalYear?.id || null,
      fiscal_year_label: fiscalYear?.label || String(currentMonth.getUTCFullYear()),
      period_id: period?.id || null,
      period_label: period?.label || `${String(currentMonth.getUTCMonth() + 1).padStart(2, "0")}/${currentMonth.getUTCFullYear()}`,
      period_is_closed: period?.is_closed ?? false,
      period_date: periodDate,
      amount_depreciation: amount,
      formatted_amount_depreciation: formatDzdAmount(amount),
      cumulated_depreciation: accumulated,
      formatted_cumulated_depreciation: formatDzdAmount(accumulated),
      net_book_value: netBookValue,
      formatted_net_book_value: formatDzdAmount(netBookValue),
      is_generated: Boolean(existingLine),
      entry_id: existingLine?.entry_id || null,
    });

    index += 1;
  }

  return {
    rows,
    summary: {
      total_rows: rows.length,
      generated_rows: rows.filter((row) => row.is_generated).length,
      projected_total: roundAmount(rows.reduce((sum, row) => sum + row.amount_depreciation, 0)),
      formatted_projected_total: formatDzdAmount(
        rows.reduce((sum, row) => sum + row.amount_depreciation, 0),
      ),
      depreciation_rate:
        asset.depreciation_method === "degressive" ? annualDegressiveRate : annualLinearRate,
      annual_rate_linear: annualLinearRate,
      annual_rate_degressive: annualDegressiveRate,
    },
  };
};

const buildPeriodsMap = (periods) =>
  new Map(periods.map((period) => [`${period.year}-${String(period.month).padStart(2, "0")}`, period]));

const buildEntryNumber = async (
  db,
  {
    companyId,
    journal,
    fiscalYearId,
    fiscalYearLabel,
    manualValue = "",
  },
) => {
  const manualEntryNumber = cleanString(manualValue);

  if (manualEntryNumber) {
    return manualEntryNumber.toUpperCase();
  }

  const fiscalSegment =
    (
      cleanString(fiscalYearLabel).replace(/[^0-9A-Za-z]+/g, "") || String(fiscalYearId)
    ).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, fiscalYearId, `${prefix}%`],
  );

  return `${prefix}${String(Number(rows[0]?.next_sequence || 1)).padStart(5, "0")}`;
};

const fetchAssetsJournal = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT id, code, label, numbering_mode, is_active
      FROM journals
      WHERE company_id = $1
        AND journal_type = 'assets'
      ORDER BY is_active DESC, code ASC
      LIMIT 1
    `,
    [companyId],
  );

  if (rows[0]) {
    return {
      ...rows[0],
      id: Number(rows[0].id),
    };
  }

  const candidateCodes = ["IMM", "IMMO", "AMOR"];

  for (const code of candidateCodes) {
    try {
      const { rows: insertedRows } = await db.query(
        `
          INSERT INTO journals (
            company_id,
            code,
            label,
            journal_type,
            default_counterpart_account,
            numbering_mode,
            is_active
          )
          VALUES ($1, $2, 'Immobilisations', 'assets', NULL, 'continuous', TRUE)
          RETURNING id, code, label, numbering_mode, is_active
        `,
        [companyId, code],
      );

      return {
        ...insertedRows[0],
        id: Number(insertedRows[0].id),
      };
    } catch (error) {
      if (error?.code !== "23505") {
        throw error;
      }
    }
  }

  throw new ApiError(
    500,
    "Impossible de créer automatiquement le journal Immobilisations.",
  );
};

const createJournalEntry = async ({
  db,
  companyId,
  journal,
  fiscalYear,
  period,
  entryDate,
  label,
  pieceRef,
  createdBy,
  lines,
}) => {
  const entryNumber = await buildEntryNumber(db, {
    companyId,
    journal,
    fiscalYearId: fiscalYear.id,
    fiscalYearLabel: fiscalYear.label,
  });
  const { rows } = await db.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
      )
      VALUES ($1, $2, $3, $4, $5, $6, $7, 'validated', $8, $9, $9)
      RETURNING id, entry_number
    `,
    [
      companyId,
      journal.id,
      fiscalYear.id,
      period.id,
      entryNumber,
      entryDate,
      label,
      pieceRef,
      createdBy,
    ],
  );

  const entryId = Number(rows[0].id);

  for (const line of lines) {
    await db.query(
      `
        INSERT INTO entry_lines (
          entry_id,
          account_id,
          auxiliary_account_id,
          label,
          debit,
          credit,
          due_date
        )
        VALUES ($1, $2, NULL, $3, $4, $5, NULL)
      `,
      [entryId, line.account_id, line.label, line.debit, line.credit],
    );
  }

  return {
    id: entryId,
    entry_number: rows[0].entry_number,
  };
};

const resolveGainLossAccounts = async (db, companyId, category) => {
  const { rows } = await db.query(
    `
      SELECT id, code, label, is_movable, is_active
      FROM accounts
      WHERE company_id = $1
        AND is_active = TRUE
        AND is_movable = TRUE
        AND (code LIKE '675%' OR code LIKE '775%' OR code LIKE '6%' OR code LIKE '7%')
      ORDER BY
        CASE
          WHEN code LIKE '675%' THEN 0
          WHEN code LIKE '775%' THEN 1
          WHEN code LIKE '6%' THEN 2
          ELSE 3
        END,
        code ASC
    `,
    [companyId],
  );

  const findByPrefix = (prefix) =>
    rows.find((row) => row.code.startsWith(prefix)) || null;

  const lossAccount =
    findByPrefix("675") ||
    findByPrefix("68") ||
    (category.account_expense ? await fetchAccountByCode(db, companyId, category.account_expense) : null) ||
    findByPrefix("6");
  const gainAccount = findByPrefix("775") || findByPrefix("7");

  if (!lossAccount) {
    throw new ApiError(400, "Aucun compte de perte sur cession n'est disponible.");
  }

  if (!gainAccount) {
    throw new ApiError(400, "Aucun compte de produit sur cession n'est disponible.");
  }

  return {
    lossAccount: {
      ...lossAccount,
      id: Number(lossAccount.id),
    },
    gainAccount: {
      ...gainAccount,
      id: Number(gainAccount.id),
    },
  };
};

const resolveTreasuryCounterpartAccount = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT id, code, label
      FROM accounts
      WHERE company_id = $1
        AND is_active = TRUE
        AND is_movable = TRUE
        AND code LIKE '5%'
      ORDER BY
        CASE
          WHEN code LIKE '512%' THEN 0
          WHEN code LIKE '53%' THEN 1
          ELSE 2
        END,
        code ASC
      LIMIT 1
    `,
    [companyId],
  );

  if (!rows[0]) {
    throw new ApiError(400, "Aucun compte de trésorerie n'est disponible pour la cession.");
  }

  return {
    ...rows[0],
    id: Number(rows[0].id),
  };
};

const buildDepreciationEntryPreview = ({ asset, category, amount, periodLabel, postingAccounts }) => ({
  label: `Dotation ${asset.code} ${periodLabel}`,
  piece_ref: `AMO-${asset.code}-${periodLabel.replace("/", "")}`,
  lines: [
    {
      account_id: postingAccounts.expenseAccount.id,
      account_code: postingAccounts.expenseAccount.code,
      account_label: postingAccounts.expenseAccount.label,
      label: `Dotation ${asset.code}`,
      debit: amount,
      credit: 0,
    },
    {
      account_id: postingAccounts.depreciationAccount.id,
      account_code: postingAccounts.depreciationAccount.code,
      account_label: postingAccounts.depreciationAccount.label,
      label: `Amortissement ${asset.code}`,
      debit: 0,
      credit: amount,
    },
  ],
  category_code: category.code,
  category_label: category.label,
});

const buildDisposalPreview = async ({ db, companyId, asset, category, disposalType, disposalDate, disposalPrice }) => {
  const postingAccounts = await fetchCategoryPostingAccounts(db, companyId, category);
  const { lossAccount, gainAccount } = await resolveGainLossAccounts(db, companyId, category);
  const treasuryAccount = disposalType === "sale"
    ? await resolveTreasuryCounterpartAccount(db, companyId)
    : null;
  const depreciationLines = await fetchDepreciationLinesByAssetId(db, asset.id);
  const fiscalYears = await fetchFiscalYears(db, companyId);
  const periods = [];

  for (const fiscalYear of fiscalYears) {
    const fiscalPeriods = await fetchPeriodsForFiscalYear(db, companyId, fiscalYear.id);
    periods.push(...fiscalPeriods);
  }

  const schedule = buildAssetSchedule({
    asset: {
      ...asset,
      disposal_date: disposalDate,
    },
    fiscalYears,
    periodsByMonthKey: buildPeriodsMap(periods),
    existingLines: depreciationLines,
  });
  const disposalMonthKey = getMonthKey(disposalDate);
  const rowAtDisposal =
    schedule.rows.find((row) => getMonthKey(row.period_date) === disposalMonthKey) ||
    schedule.rows.at(-1) ||
    null;
  const accumulatedDepreciation = rowAtDisposal
    ? roundAmount(rowAtDisposal.cumulated_depreciation)
    : roundAmount(asset.accumulated_depreciation);
  const netBookValue = rowAtDisposal
    ? roundAmount(rowAtDisposal.net_book_value)
    : roundAmount(asset.net_book_value);
  const gainLoss = roundAmount(disposalPrice - netBookValue);
  const disposalStatus = disposalType === "sale" ? "sold" : "scrapped";
  const lines = [];

  if (accumulatedDepreciation > 0) {
    lines.push({
      account_id: postingAccounts.depreciationAccount.id,
      account_code: postingAccounts.depreciationAccount.code,
      account_label: postingAccounts.depreciationAccount.label,
      label: `Reprise amortissement ${asset.code}`,
      debit: accumulatedDepreciation,
      credit: 0,
    });
  }

  if (disposalType === "sale" && disposalPrice > 0) {
    lines.push({
      account_id: treasuryAccount.id,
      account_code: treasuryAccount.code,
      account_label: treasuryAccount.label,
      label: `Encaissement cession ${asset.code}`,
      debit: disposalPrice,
      credit: 0,
    });
  }

  if (gainLoss < 0) {
    lines.push({
      account_id: lossAccount.id,
      account_code: lossAccount.code,
      account_label: lossAccount.label,
      label: `Perte cession ${asset.code}`,
      debit: Math.abs(gainLoss),
      credit: 0,
    });
  }

  lines.push({
    account_id: postingAccounts.assetAccount.id,
    account_code: postingAccounts.assetAccount.code,
    account_label: postingAccounts.assetAccount.label,
    label: `Sortie immobilisation ${asset.code}`,
    debit: 0,
    credit: roundAmount(asset.acquisition_value),
  });

  if (gainLoss > 0) {
    lines.push({
      account_id: gainAccount.id,
      account_code: gainAccount.code,
      account_label: gainAccount.label,
      label: `Plus-value cession ${asset.code}`,
      debit: 0,
      credit: gainLoss,
    });
  }

  return {
    status_after_disposal: disposalStatus,
    status_after_disposal_label: ASSET_STATUS_LABELS[disposalStatus],
    disposal_date: disposalDate,
    disposal_type: disposalType,
    disposal_type_label: DISPOSAL_TYPE_LABELS[disposalType],
    disposal_price: disposalPrice,
    formatted_disposal_price: formatDzdAmount(disposalPrice),
    accumulated_depreciation: accumulatedDepreciation,
    formatted_accumulated_depreciation: formatDzdAmount(accumulatedDepreciation),
    net_book_value: netBookValue,
    formatted_net_book_value: formatDzdAmount(netBookValue),
    gain_loss: gainLoss,
    formatted_gain_loss: formatDzdAmount(gainLoss),
    lines,
    label: `${DISPOSAL_TYPE_LABELS[disposalType]} ${asset.code}`,
    piece_ref: `DISP-${asset.code}-${disposalDate.replace(/-/g, "")}`,
  };
};

const createDepreciationLineAndEntry = async ({
  db,
  companyId,
  asset,
  category,
  fiscalYear,
  period,
  scheduleRow,
  userId,
}) => {
  const journal = await fetchAssetsJournal(db, companyId);
  const postingAccounts = await fetchCategoryPostingAccounts(db, companyId, category);
  const preview = buildDepreciationEntryPreview({
    asset,
    category,
    amount: scheduleRow.amount_depreciation,
    periodLabel: scheduleRow.period_label,
    postingAccounts,
  });
  const entry = await createJournalEntry({
    db,
    companyId,
    journal,
    fiscalYear,
    period,
    entryDate: scheduleRow.period_date,
    label: preview.label,
    pieceRef: preview.piece_ref,
    createdBy: userId,
    lines: preview.lines,
  });

  const { rows } = await db.query(
    `
      INSERT INTO depreciation_lines (
        asset_id,
        fiscal_year_id,
        period_date,
        amount_depreciation,
        cumulated_depreciation,
        net_book_value,
        entry_id
      )
      VALUES ($1, $2, $3, $4, $5, $6, $7)
      RETURNING id
    `,
    [
      asset.id,
      fiscalYear.id,
      scheduleRow.period_date,
      scheduleRow.amount_depreciation,
      scheduleRow.cumulated_depreciation,
      scheduleRow.net_book_value,
      entry.id,
    ],
  );

  return {
    depreciation_line_id: Number(rows[0].id),
    entry_id: entry.id,
    entry_number: entry.entry_number,
    preview,
  };
};

const buildAssetOptions = async (db, companyId) => {
  const [categories, accounts, fiscalYears] = await Promise.all([
    fetchAssetCategoryList(db, companyId),
    db.query(
      `
        SELECT id, code, label, is_active, is_movable
        FROM accounts
        WHERE company_id = $1
          AND is_active = TRUE
        ORDER BY code ASC
      `,
      [companyId],
    ),
    fetchFiscalYears(db, companyId),
  ]);

  return {
    categories,
    account_options: accounts.rows.map((row) => ({
      id: Number(row.id),
      code: row.code,
      label: row.label,
      is_movable: row.is_movable,
    })),
    fiscal_years: fiscalYears,
  };
};

export const assetCategoriesController = {
  list: async (req, res) => {
    const companyId = getCompanyId(req);

    res.json({
      company_id: companyId,
      items: await fetchAssetCategoryList(pool, companyId),
    });
  },

  create: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    const client = await pool.connect();

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

      const payload = await normalizeCategoryPayload({
        db: client,
        companyId,
        body: req.body,
      });
      const { rows } = await client.query(
        `
          INSERT INTO asset_categories (
            company_id,
            code,
            label,
            depreciation_method,
            default_duration_years,
            account_asset,
            account_depreciation,
            account_expense
          )
          VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
          RETURNING *
        `,
        [
          companyId,
          payload.code,
          payload.label,
          payload.depreciation_method,
          payload.default_duration_years,
          payload.account_asset,
          payload.account_depreciation,
          payload.account_expense,
        ],
      );

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "asset_categories.create",
        entityType: "asset_category",
        entityId: rows[0].id,
        newData: serializeCategoryForAudit(rows[0]),
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.status(201).json({
        message: "Catégorie créée avec succès.",
        item: await fetchAssetCategoryById(pool, companyId, rows[0].id),
      });
    } catch (error) {
      await client.query("ROLLBACK");

      if (error?.code === "23505") {
        throw new ApiError(409, "Ce code de catégorie existe déjà.");
      }

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

  update: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    const categoryId = Number(req.params.id);

    if (!categoryId) {
      throw new ApiError(400, "Identifiant de catégorie invalide.");
    }

    const client = await pool.connect();

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

      const existingCategory = await fetchAssetCategoryById(client, companyId, categoryId);

      if (!existingCategory) {
        throw new ApiError(404, "Catégorie introuvable.");
      }

      const payload = await normalizeCategoryPayload({
        db: client,
        companyId,
        body: req.body,
        existingCategory,
      });
      const { rows } = await client.query(
        `
          UPDATE asset_categories
          SET
            code = $3,
            label = $4,
            depreciation_method = $5,
            default_duration_years = $6,
            account_asset = $7,
            account_depreciation = $8,
            account_expense = $9
          WHERE company_id = $1
            AND id = $2
          RETURNING *
        `,
        [
          companyId,
          categoryId,
          payload.code,
          payload.label,
          payload.depreciation_method,
          payload.default_duration_years,
          payload.account_asset,
          payload.account_depreciation,
          payload.account_expense,
        ],
      );

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "asset_categories.update",
        entityType: "asset_category",
        entityId: categoryId,
        oldData: serializeCategoryForAudit(existingCategory),
        newData: serializeCategoryForAudit(rows[0]),
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.json({
        message: "Catégorie mise à jour avec succès.",
        item: await fetchAssetCategoryById(pool, companyId, categoryId),
      });
    } catch (error) {
      await client.query("ROLLBACK");

      if (error?.code === "23505") {
        throw new ApiError(409, "Ce code de catégorie existe déjà.");
      }

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

  remove: async (req, res) => {
    const companyId = getCompanyId(req);
    const categoryId = Number(req.params.id);

    if (!categoryId) {
      throw new ApiError(400, "Identifiant de catégorie invalide.");
    }

    const client = await pool.connect();

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

      const existingCategory = await fetchAssetCategoryById(client, companyId, categoryId);

      if (!existingCategory) {
        throw new ApiError(404, "Catégorie introuvable.");
      }

      const { rows: usageRows } = await client.query(
        `
          SELECT COUNT(*)::int AS linked_assets
          FROM assets
          WHERE company_id = $1
            AND category_id = $2
        `,
        [companyId, categoryId],
      );

      if (Number(usageRows[0]?.linked_assets || 0) > 0) {
        throw new ApiError(
          409,
          "Cette catégorie est utilisée par des immobilisations existantes.",
        );
      }

      await client.query(
        `
          DELETE FROM asset_categories
          WHERE company_id = $1
            AND id = $2
        `,
        [companyId, categoryId],
      );

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "asset_categories.delete",
        entityType: "asset_category",
        entityId: categoryId,
        oldData: serializeCategoryForAudit(existingCategory),
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.status(204).send();
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },
};

export const assetsController = {
  list: async (req, res) => {
    const companyId = getCompanyId(req);
    const items = await fetchAssetsSnapshot(pool, companyId, {
      filters: req.query,
    });

    res.json({
      company_id: companyId,
      items,
      meta: await buildAssetOptions(pool, companyId),
    });
  },

  getById: async (req, res) => {
    const companyId = getCompanyId(req);
    const assetId = Number(req.params.id);

    if (!assetId) {
      throw new ApiError(400, "Identifiant d'immobilisation invalide.");
    }

    const item = (await fetchAssetsSnapshot(pool, companyId, { assetId }))[0];

    if (!item) {
      throw new ApiError(404, "Immobilisation introuvable.");
    }

    res.json({
      item,
      meta: await buildAssetOptions(pool, companyId),
    });
  },

  create: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    const client = await pool.connect();

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

      const payload = await normalizeAssetPayload({
        db: client,
        companyId,
        body: req.body,
      });
      const { rows } = await client.query(
        `
          INSERT INTO assets (
            company_id,
            category_id,
            code,
            label,
            acquisition_date,
            acquisition_value,
            residual_value,
            duration_years,
            depreciation_method,
            assignment_center,
            status,
            description,
            depreciation_start_date
          )
          VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
          RETURNING *
        `,
        [
          companyId,
          payload.category_id,
          payload.code,
          payload.label,
          payload.acquisition_date,
          payload.acquisition_value,
          payload.residual_value,
          payload.duration_years,
          payload.depreciation_method,
          payload.assignment_center,
          payload.status,
          payload.description,
          payload.depreciation_start_date,
        ],
      );

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "assets.create",
        entityType: "asset",
        entityId: rows[0].id,
        newData: serializeAssetForAudit(rows[0]),
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.status(201).json({
        message: "Immobilisation créée avec succès.",
        item: (await fetchAssetsSnapshot(pool, companyId, { assetId: rows[0].id }))[0],
      });
    } catch (error) {
      await client.query("ROLLBACK");

      if (error?.code === "23505") {
        throw new ApiError(409, "Ce code d'immobilisation existe déjà.");
      }

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

  update: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    const assetId = Number(req.params.id);

    if (!assetId) {
      throw new ApiError(400, "Identifiant d'immobilisation invalide.");
    }

    const client = await pool.connect();

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

      const existingAsset = (await fetchAssetsSnapshot(client, companyId, { assetId }))[0];

      if (!existingAsset) {
        throw new ApiError(404, "Immobilisation introuvable.");
      }

      if (existingAsset.status !== "active") {
        throw new ApiError(
          409,
          "Une immobilisation sortie ne peut plus être modifiée via ce formulaire.",
        );
      }

      const payload = await normalizeAssetPayload({
        db: client,
        companyId,
        body: req.body,
        existingAsset,
      });
      const { rows } = await client.query(
        `
          UPDATE assets
          SET
            category_id = $3,
            code = $4,
            label = $5,
            acquisition_date = $6,
            acquisition_value = $7,
            residual_value = $8,
            duration_years = $9,
            depreciation_method = $10,
            assignment_center = $11,
            description = $12,
            depreciation_start_date = $13
          WHERE company_id = $1
            AND id = $2
          RETURNING *
        `,
        [
          companyId,
          assetId,
          payload.category_id,
          payload.code,
          payload.label,
          payload.acquisition_date,
          payload.acquisition_value,
          payload.residual_value,
          payload.duration_years,
          payload.depreciation_method,
          payload.assignment_center,
          payload.description,
          payload.depreciation_start_date,
        ],
      );

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "assets.update",
        entityType: "asset",
        entityId: assetId,
        oldData: serializeAssetForAudit(existingAsset),
        newData: serializeAssetForAudit(rows[0]),
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.json({
        message: "Immobilisation mise à jour avec succès.",
        item: (await fetchAssetsSnapshot(pool, companyId, { assetId }))[0],
      });
    } catch (error) {
      await client.query("ROLLBACK");

      if (error?.code === "23505") {
        throw new ApiError(409, "Ce code d'immobilisation existe déjà.");
      }

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

  remove: async (req, res) => {
    const companyId = getCompanyId(req);
    const assetId = Number(req.params.id);

    if (!assetId) {
      throw new ApiError(400, "Identifiant d'immobilisation invalide.");
    }

    const client = await pool.connect();

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

      const existingAsset = (await fetchAssetsSnapshot(client, companyId, { assetId }))[0];

      if (!existingAsset) {
        throw new ApiError(404, "Immobilisation introuvable.");
      }

      const { rows: depRows } = await client.query(
        `
          SELECT COUNT(*)::int AS count
          FROM depreciation_lines
          WHERE asset_id = $1
        `,
        [assetId],
      );

      if (Number(depRows[0]?.count || 0) > 0 || existingAsset.disposed_entry_id) {
        throw new ApiError(
          409,
          "Cette immobilisation possède déjà des dotations ou une sortie comptabilisée.",
        );
      }

      await client.query(
        `
          DELETE FROM assets
          WHERE company_id = $1
            AND id = $2
        `,
        [companyId, assetId],
      );

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "assets.delete",
        entityType: "asset",
        entityId: assetId,
        oldData: serializeAssetForAudit(existingAsset),
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.status(204).send();
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

  getDepreciationSchedule: async (req, res) => {
    const companyId = getCompanyId(req);
    const assetId = Number(req.params.id);

    if (!assetId) {
      throw new ApiError(400, "Identifiant d'immobilisation invalide.");
    }

    const asset = (await fetchAssetsSnapshot(pool, companyId, { assetId }))[0];

    if (!asset) {
      throw new ApiError(404, "Immobilisation introuvable.");
    }

    const fiscalYears = await fetchFiscalYears(pool, companyId);
    const depreciationLines = await fetchDepreciationLinesByAssetId(pool, assetId);
    const periods = [];

    for (const fiscalYear of fiscalYears) {
      const fiscalPeriods = await fetchPeriodsForFiscalYear(pool, companyId, fiscalYear.id);
      periods.push(...fiscalPeriods);
    }

    const schedule = buildAssetSchedule({
      asset,
      fiscalYears,
      periodsByMonthKey: buildPeriodsMap(periods),
      existingLines: depreciationLines,
    });

    res.json({
      company_id: companyId,
      item: asset,
      schedule: schedule.rows,
      summary: schedule.summary,
      fiscal_years: fiscalYears,
    });
  },

  generateDepreciations: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    const assetId = Number(req.params.id);
    const fiscalYearId = Number(req.body.fiscal_year_id || req.query.fiscal_year_id || 0);

    if (!assetId) {
      throw new ApiError(400, "Identifiant d'immobilisation invalide.");
    }

    if (!fiscalYearId) {
      throw new ApiError(400, "L'exercice est obligatoire.");
    }

    const client = await pool.connect();

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

      const asset = (await fetchAssetsSnapshot(client, companyId, { assetId }))[0];

      if (!asset) {
        throw new ApiError(404, "Immobilisation introuvable.");
      }

      const fiscalYear = await fetchFiscalYearById(client, companyId, fiscalYearId);

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

      const fiscalYears = await fetchFiscalYears(client, companyId);
      const depreciationLines = await fetchDepreciationLinesByAssetId(client, assetId);
      const periods = await fetchPeriodsForFiscalYear(client, companyId, fiscalYearId);
      const periodsByMonthKey = buildPeriodsMap(periods);
      const schedule = buildAssetSchedule({
        asset,
        fiscalYears,
        periodsByMonthKey,
        existingLines: depreciationLines,
      });
      const rowsToGenerate = schedule.rows.filter(
        (row) =>
          Number(row.fiscal_year_id) === fiscalYearId &&
          !row.is_generated &&
          row.amount_depreciation > 0,
      );

      if (!rowsToGenerate.length) {
        throw new ApiError(400, "Aucune dotation à générer pour cet exercice.");
      }

      const category = await fetchAssetCategoryById(client, companyId, asset.category_id);
      const createdItems = [];
      const skippedItems = [];

      for (const row of rowsToGenerate) {
        const period = periods.find((periodItem) => Number(periodItem.id) === Number(row.period_id));

        if (!period || period.is_closed || fiscalYear.is_closed) {
          skippedItems.push({
            period_label: row.period_label,
            reason: "Période clôturée ou indisponible.",
          });
          continue;
        }

        const created = await createDepreciationLineAndEntry({
          db: client,
          companyId,
          asset,
          category,
          fiscalYear,
          period,
          scheduleRow: row,
          userId: req.user?.sub || null,
        });

        createdItems.push({
          period_label: row.period_label,
          amount_depreciation: row.amount_depreciation,
          formatted_amount_depreciation: row.formatted_amount_depreciation,
          entry_id: created.entry_id,
          entry_number: created.entry_number,
        });
      }

      if (!createdItems.length) {
        throw new ApiError(400, "Aucune dotation n'a pu être générée sur des périodes ouvertes.");
      }

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "assets.generate_depreciations",
        entityType: "asset",
        entityId: assetId,
        newData: {
          fiscal_year_id: fiscalYearId,
          created_count: createdItems.length,
          skipped_count: skippedItems.length,
        },
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.status(201).json({
        message: "Dotations générées avec succès.",
        asset_id: assetId,
        fiscal_year_id: fiscalYearId,
        created_count: createdItems.length,
        skipped_count: skippedItems.length,
        items: createdItems,
        skipped_items: skippedItems,
      });
    } catch (error) {
      await client.query("ROLLBACK");

      if (error?.code === "23505") {
        throw new ApiError(409, "Une dotation existe déjà pour cette période.");
      }

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

  dispose: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    const assetId = Number(req.params.id);
    const previewOnly = Boolean(req.body.preview);

    if (!assetId) {
      throw new ApiError(400, "Identifiant d'immobilisation invalide.");
    }

    const client = await pool.connect();

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

      const asset = (await fetchAssetsSnapshot(client, companyId, { assetId }))[0];

      if (!asset) {
        throw new ApiError(404, "Immobilisation introuvable.");
      }

      if (asset.status !== "active") {
        throw new ApiError(409, "Cette immobilisation est déjà sortie.");
      }

      const disposalType = normalizeDisposalType(req.body.type);
      const disposalDate = parseDateInput(req.body.disposal_date, "La date de cession");
      const disposalPrice =
        disposalType === "sale"
          ? parseCurrency(req.body.disposal_price, "Le prix de cession")
          : 0;

      if (disposalDate < asset.acquisition_date) {
        throw new ApiError(400, "La date de sortie ne peut pas être antérieure à l'acquisition.");
      }

      const period = await fetchPeriodById(
        client,
        companyId,
        req.body.period_id || req.query.period_id || 0,
      );
      const effectivePeriod =
        period ||
        (await (async () => {
          const fiscalYears = await fetchFiscalYears(client, companyId);
          const fiscalYear = findFiscalYearForDate(fiscalYears, disposalDate);

          if (!fiscalYear) {
            return null;
          }

          const fiscalPeriods = await fetchPeriodsForFiscalYear(client, companyId, fiscalYear.id);

          return (
            fiscalPeriods.find(
              (periodItem) =>
                periodItem.month === Number(disposalDate.slice(5, 7)) &&
                periodItem.year === Number(disposalDate.slice(0, 4)),
            ) || null
          );
        })());

      if (!effectivePeriod) {
        throw new ApiError(400, "La date de sortie doit appartenir à une période comptable existante.");
      }

      if (effectivePeriod.is_closed || effectivePeriod.fiscal_year_is_closed) {
        throw new ApiError(400, "La sortie doit être comptabilisée sur une période ouverte.");
      }

      const category = await fetchAssetCategoryById(client, companyId, asset.category_id);
      const preview = await buildDisposalPreview({
        db: client,
        companyId,
        asset,
        category,
        disposalType,
        disposalDate,
        disposalPrice,
      });

      if (previewOnly) {
        await client.query("ROLLBACK");

        res.json({
          company_id: companyId,
          asset_id: assetId,
          preview,
        });
        return;
      }

      const fiscalYear = await fetchFiscalYearById(client, companyId, effectivePeriod.fiscal_year_id);
      const journal = await fetchAssetsJournal(client, companyId);
      const entry = await createJournalEntry({
        db: client,
        companyId,
        journal,
        fiscalYear,
        period: {
          id: effectivePeriod.id,
        },
        entryDate: disposalDate,
        label: preview.label,
        pieceRef: preview.piece_ref,
        createdBy: req.user?.sub || null,
        lines: preview.lines,
      });

      await client.query(
        `
          UPDATE assets
          SET
            status = $3,
            disposal_date = $4,
            disposal_type = $5,
            disposal_price = $6,
            disposal_gain_loss = $7,
            disposed_entry_id = $8
          WHERE company_id = $1
            AND id = $2
        `,
        [
          companyId,
          assetId,
          preview.status_after_disposal,
          disposalDate,
          disposalType,
          disposalPrice,
          preview.gain_loss,
          entry.id,
        ],
      );

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "assets.dispose",
        entityType: "asset",
        entityId: assetId,
        newData: {
          disposal_type: disposalType,
          disposal_date: disposalDate,
          disposal_price: disposalPrice,
          gain_loss: preview.gain_loss,
          entry_id: entry.id,
        },
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.status(201).json({
        message:
          disposalType === "sale"
            ? "Cession comptabilisée avec succès."
            : "Mise au rebut comptabilisée avec succès.",
        asset_id: assetId,
        entry_id: entry.id,
        entry_number: entry.entry_number,
        preview,
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },
};

export const depreciationRunController = {
  execute: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    const fiscalYearId = Number(req.query.fiscal_year_id || req.body.fiscal_year_id || 0);
    const periodId = Number(req.query.period_id || req.body.period_id || 0);
    const previewOnly =
      req.query.preview === "true" || req.body.preview === true || req.body.mode === "preview";

    if (!fiscalYearId || !periodId) {
      throw new ApiError(400, "L'exercice et la période sont obligatoires.");
    }

    const client = await pool.connect();

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

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

      if (!fiscalYear || !period || Number(period.fiscal_year_id) !== fiscalYearId) {
        throw new ApiError(404, "Exercice ou période introuvable.");
      }

      if (period.is_closed || fiscalYear.is_closed) {
        throw new ApiError(400, "La période sélectionnée est clôturée.");
      }

      const assets = await fetchAssetsSnapshot(client, companyId, {
        filters: {
          status: "active",
        },
      });
      const fiscalYears = await fetchFiscalYears(client, companyId);
      const allPeriods = await fetchPeriodsForFiscalYear(client, companyId, fiscalYearId);
      const periodsByMonthKey = buildPeriodsMap(allPeriods);
      const targetMonthKey = `${period.year}-${String(period.month).padStart(2, "0")}`;
      const items = [];

      for (const asset of assets) {
        const depreciationLines = await fetchDepreciationLinesByAssetId(client, asset.id);
        const schedule = buildAssetSchedule({
          asset,
          fiscalYears,
          periodsByMonthKey,
          existingLines: depreciationLines,
        });
        const scheduleRow = schedule.rows.find(
          (row) =>
            row.fiscal_year_id === fiscalYearId &&
            getMonthKey(row.period_date) === targetMonthKey &&
            !row.is_generated &&
            row.amount_depreciation > 0,
        );

        if (!scheduleRow) {
          continue;
        }

        const category = await fetchAssetCategoryById(client, companyId, asset.category_id);
        const postingAccounts = await fetchCategoryPostingAccounts(client, companyId, category);
        const preview = buildDepreciationEntryPreview({
          asset,
          category,
          amount: scheduleRow.amount_depreciation,
          periodLabel: period.label,
          postingAccounts,
        });

        items.push({
          asset_id: asset.id,
          asset_code: asset.code,
          asset_label: asset.label,
          category_code: asset.category.code,
          category_label: asset.category.label,
          amount_depreciation: scheduleRow.amount_depreciation,
          formatted_amount_depreciation: scheduleRow.formatted_amount_depreciation,
          period_date: scheduleRow.period_date,
          journal_entry_preview: preview,
        });
      }

      if (previewOnly) {
        await client.query("ROLLBACK");

        res.json({
          company_id: companyId,
          fiscal_year_id: fiscalYearId,
          period_id: periodId,
          period_label: period.label,
          preview: true,
          items,
          count: items.length,
        });
        return;
      }

      if (!items.length) {
        throw new ApiError(400, "Aucune dotation à générer sur la période sélectionnée.");
      }

      const createdItems = [];

      for (const item of items) {
        const asset = assets.find((assetItem) => assetItem.id === item.asset_id);
        const category = await fetchAssetCategoryById(client, companyId, asset.category_id);
        const created = await createDepreciationLineAndEntry({
          db: client,
          companyId,
          asset,
          category,
          fiscalYear,
          period,
          scheduleRow: {
            period_date: item.period_date,
            period_label: period.label,
            amount_depreciation: item.amount_depreciation,
            cumulated_depreciation: roundAmount(
              (await fetchDepreciationLinesByAssetId(client, item.asset_id)).reduce(
                (sum, line) => sum + line.amount_depreciation,
                0,
              ) + item.amount_depreciation,
            ),
            net_book_value: roundAmount(asset.acquisition_value -
              (
                (await fetchDepreciationLinesByAssetId(client, item.asset_id)).reduce(
                  (sum, line) => sum + line.amount_depreciation,
                  0,
                ) + item.amount_depreciation
              )),
          },
          userId: req.user?.sub || null,
        });

        createdItems.push({
          asset_id: item.asset_id,
          asset_code: item.asset_code,
          amount_depreciation: item.amount_depreciation,
          entry_id: created.entry_id,
          entry_number: created.entry_number,
        });
      }

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "depreciation_run.execute",
        entityType: "fiscal_year",
        entityId: fiscalYearId,
        newData: {
          period_id: periodId,
          created_count: createdItems.length,
        },
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      const notifications = await createCompanyNotifications(pool, {
        companyId,
        type: "asset_depreciation_run",
        message: `Dotations générées pour ${createdItems.length} immobilisation(s)`,
        entityId: periodId,
      });

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

      res.status(201).json({
        company_id: companyId,
        fiscal_year_id: fiscalYearId,
        period_id: periodId,
        preview: false,
        created_count: createdItems.length,
        items: createdItems,
      });
    } catch (error) {
      await client.query("ROLLBACK");

      if (error?.code === "23505") {
        throw new ApiError(409, "Certaines dotations existent déjà pour cette période.");
      }

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