import XLSX from "xlsx";

import { pool } from "../db/pool.js";
import { ApiError } from "../utils/ApiError.js";
import { logAudit } from "../utils/auditLog.js";

const ACCOUNT_CODE_REGEX = /^[1-7][0-9]{0,19}$/;
const MAX_IMPORT_PREVIEW_ROWS = 20;

const ACCOUNT_CLASS_LABELS = {
  "1": "Capitaux, provisions et dettes financieres",
  "2": "Immobilisations",
  "3": "Stocks et en-cours",
  "4": "Tiers",
  "5": "Tresorerie",
  "6": "Charges",
  "7": "Produits",
};

const TYPE_LABELS = {
  bilan: "Bilan",
  gestion: "Gestion",
  asset: "Actif",
  liability: "Passif",
  expense: "Charge",
  revenue: "Produit",
  treasury: "Trésorerie",
};

const SIDE_LABELS = {
  debit: "Débiteur",
  credit: "Créditeur",
};

const BOOLEAN_LABELS = {
  true: "Oui",
  false: "Non",
};

const COLUMN_ALIASES = {
  code: ["code", "compte", "account_code", "numero", "numcompte", "ncompte"],
  label: ["label", "libelle", "intitule", "designation", "name", "nom"],
  type: ["type", "account_type", "nature", "categorie"],
  parent_code: ["parent_code", "parent", "compte_parent", "code_parent"],
};

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

const normalizeKey = (value) =>
  cleanString(value)
    .normalize("NFD")
    .replace(/\p{Diacritic}/gu, "")
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "");

const parseBoolean = (value, fallback = false) => {
  if (typeof value === "boolean") {
    return value;
  }

  if (value === null || value === undefined || value === "") {
    return fallback;
  }

  const normalized = cleanString(value).toLowerCase();

  if (["true", "1", "yes", "oui", "active", "actif"].includes(normalized)) {
    return true;
  }

  if (["false", "0", "no", "non", "inactive", "inactif"].includes(normalized)) {
    return false;
  }

  return fallback;
};

const getStatementType = (classCode) =>
  ["1", "2", "3", "4", "5"].includes(String(classCode)) ? "bilan" : "gestion";

const inferUiTypeFromStored = ({ accountType, classCode, balanceNormalSide }) => {
  if (accountType === "treasury") {
    return "treasury";
  }

  if (accountType === "expense") {
    return "expense";
  }

  if (accountType === "revenue") {
    return "revenue";
  }

  if (["equity", "liability"].includes(accountType)) {
    return "liability";
  }

  if (["fixed_asset", "financial_asset", "inventory", "contra_asset", "asset"].includes(accountType)) {
    return "asset";
  }

  if (accountType === "third_party") {
    return balanceNormalSide === "credit" ? "liability" : "asset";
  }

  if (classCode === "5") {
    return "treasury";
  }

  if (classCode === "6") {
    return "expense";
  }

  if (classCode === "7") {
    return "revenue";
  }

  return balanceNormalSide === "credit" ? "liability" : "asset";
};

const normalizeUiType = (value) => {
  const normalized = cleanString(value).toLowerCase();

  if (!normalized) {
    return null;
  }

  const mappings = new Map([
    ["actif", "asset"],
    ["asset", "asset"],
    ["immobilisation", "asset"],
    ["passif", "liability"],
    ["liability", "liability"],
    ["charge", "expense"],
    ["charges", "expense"],
    ["expense", "expense"],
    ["produit", "revenue"],
    ["produits", "revenue"],
    ["revenue", "revenue"],
    ["tresorerie", "treasury"],
    ["trésorerie", "treasury"],
    ["treasury", "treasury"],
    ["fixed_asset", "asset"],
    ["financial_asset", "asset"],
    ["inventory", "asset"],
    ["asset_account", "asset"],
    ["equity", "liability"],
    ["third_party", "asset"],
  ]);

  return mappings.get(normalized) || null;
};

const normalizeBalanceSide = (value) => {
  const normalized = cleanString(value).toLowerCase();

  if (["debit", "debiteur", "débiteur", "debiteur ", "d"].includes(normalized)) {
    return "debit";
  }

  if (["credit", "crediteur", "créditeur", "c"].includes(normalized)) {
    return "credit";
  }

  return null;
};

const normalizeThirdPartyType = (value) => {
  const normalized = cleanString(value).toLowerCase();

  if (["client", "customer"].includes(normalized)) {
    return "client";
  }

  if (["fournisseur", "supplier"].includes(normalized)) {
    return "supplier";
  }

  return null;
};

const inferBalanceSide = ({ uiType, classCode, existingSide }) => {
  if (existingSide) {
    return existingSide;
  }

  if (uiType === "liability" || uiType === "revenue") {
    return "credit";
  }

  if (uiType === "expense" || uiType === "asset" || uiType === "treasury") {
    return "debit";
  }

  if (classCode === "1" || classCode === "7") {
    return "credit";
  }

  return "debit";
};

const mapUiTypeToStoredType = ({ uiType, classCode, code, balanceNormalSide }) => {
  if (uiType === "treasury") {
    return "treasury";
  }

  if (uiType === "expense") {
    return "expense";
  }

  if (uiType === "revenue") {
    return "revenue";
  }

  if (uiType === "liability") {
    if (classCode === "1" && /^1(0|1|2)/.test(code)) {
      return "equity";
    }

    if (classCode === "4") {
      return "third_party";
    }

    return "liability";
  }

  if (uiType === "asset") {
    if (classCode === "2") {
      return "fixed_asset";
    }

    if (classCode === "3") {
      return "inventory";
    }

    if (classCode === "4") {
      return "third_party";
    }

    if (classCode === "5") {
      return "treasury";
    }

    return balanceNormalSide === "credit" ? "liability" : "asset";
  }

  return "asset";
};

const inferThirdPartyType = ({ code, balanceNormalSide, isAuxiliary }) => {
  if (!isAuxiliary) {
    return null;
  }

  if (/^(40|401|404)/.test(code)) {
    return "supplier";
  }

  if (/^(41|411|416|418)/.test(code)) {
    return "client";
  }

  return balanceNormalSide === "credit" ? "supplier" : "client";
};

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

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

  return companyId;
};

const parseImportMode = (value) => {
  const normalized = cleanString(value).toLowerCase();

  if (["inspect", "preview", "commit"].includes(normalized)) {
    return normalized;
  }

  return "inspect";
};

const parseImportMapping = (rawValue) => {
  if (!rawValue) {
    return {};
  }

  if (typeof rawValue === "object" && !Array.isArray(rawValue)) {
    return rawValue;
  }

  try {
    return JSON.parse(rawValue);
  } catch (error) {
    throw new ApiError(400, "Invalid import mapping payload.");
  }
};

const readSpreadsheet = (file) => {
  if (!file?.buffer?.length) {
    throw new ApiError(400, "Le fichier d'import est requis.");
  }

  const workbook = XLSX.read(file.buffer, {
    type: "buffer",
    raw: false,
  });
  const firstSheetName = workbook.SheetNames[0];

  if (!firstSheetName) {
    throw new ApiError(400, "Le fichier importé est vide.");
  }

  const sheet = workbook.Sheets[firstSheetName];
  const matrix = XLSX.utils.sheet_to_json(sheet, {
    header: 1,
    defval: "",
    blankrows: false,
    raw: false,
  });

  if (!matrix.length) {
    throw new ApiError(400, "Le fichier importé est vide.");
  }

  const headerRow = matrix[0].map((column, index) => cleanString(column) || `column_${index + 1}`);
  const rows = matrix.slice(1).map((rowValues) =>
    headerRow.reduce((accumulator, columnName, index) => {
      accumulator[columnName] = rowValues[index] ?? "";
      return accumulator;
    }, {}),
  );

  return {
    columns: headerRow,
    rows,
  };
};

const detectSuggestedMapping = (columns) => {
  const suggestions = {};

  Object.entries(COLUMN_ALIASES).forEach(([targetField, aliases]) => {
    const match = columns.find((columnName) =>
      aliases.includes(normalizeKey(columnName)),
    );

    if (match) {
      suggestions[targetField] = match;
    }
  });

  return suggestions;
};

const findClosestPrefixCode = ({ code, batchCodes, existingAccountsByCode }) => {
  for (let index = code.length - 1; index > 0; index -= 1) {
    const candidateCode = code.slice(0, index);

    if (batchCodes.has(candidateCode) || existingAccountsByCode.has(candidateCode)) {
      return candidateCode;
    }
  }

  return null;
};

const getImportContextFromAccount = (account) => {
  if (!account) {
    return null;
  }

  return {
    ui_type: inferUiTypeFromStored({
      accountType: account.account_type,
      classCode: cleanString(account.code).charAt(0),
      balanceNormalSide: account.balance_normal_side,
    }),
    balance_normal_side: account.balance_normal_side,
  };
};

const matchesImportLabelHint = (label, hints) => {
  const normalizedLabel = normalizeKey(label);

  return hints.some((hint) => normalizedLabel.includes(normalizeKey(hint)));
};

const inferImportUiType = ({
  code,
  label,
  classCode,
  existingContext,
  parentContext,
}) => {
  if (existingContext?.ui_type) {
    return existingContext.ui_type;
  }

  if (classCode === "7") {
    return "revenue";
  }

  if (classCode === "6") {
    return "expense";
  }

  if (classCode === "5") {
    return "treasury";
  }

  if (classCode === "2" || classCode === "3") {
    return "asset";
  }

  if (classCode === "1") {
    if (
      /^109/.test(code) ||
      /^133/.test(code) ||
      matchesImportLabelHint(label, ["actif", "non appele", "déficit", "deficit", "perte"])
    ) {
      return "asset";
    }

    return "liability";
  }

  if (classCode === "4") {
    if (/^(40|404|408|409)/.test(code)) {
      return "liability";
    }

    if (/^(41|416|418)/.test(code)) {
      return "asset";
    }

    if (
      matchesImportLabelHint(label, [
        "debiteur",
        "débiteur",
        "a recevoir",
        "à recevoir",
        "deductible",
        "déductible",
        "avance",
        "acomptes verses",
        "acompte verse",
        "creance",
        "créance",
      ])
    ) {
      return "asset";
    }

    if (
      matchesImportLabelHint(label, [
        "crediteur",
        "créditeur",
        "a payer",
        "à payer",
        "dettes",
        "fournisseur",
        "collectee",
        "collectée",
      ])
    ) {
      return "liability";
    }
  }

  return parentContext?.ui_type || "asset";
};

const inferImportBalanceSide = ({
  code,
  label,
  classCode,
  uiType,
  existingContext,
  parentContext,
}) => {
  if (existingContext?.balance_normal_side) {
    return existingContext.balance_normal_side;
  }

  if (classCode === "7") {
    return "credit";
  }

  if (classCode === "6") {
    return "debit";
  }

  if (/^(28|29|39)/.test(code)) {
    return "credit";
  }

  if (classCode === "5") {
    return /^519/.test(code) ? "credit" : "debit";
  }

  if (classCode === "1") {
    if (
      /^109/.test(code) ||
      /^133/.test(code) ||
      matchesImportLabelHint(label, ["actif", "non appele", "déficit", "deficit", "perte"])
    ) {
      return "debit";
    }

    return "credit";
  }

  if (classCode === "4") {
    if (/^(40|404|408|409|421|422|428|43|44|45|47)/.test(code)) {
      return "credit";
    }

    if (/^(41|416|418|425|426|46|48)/.test(code)) {
      return "debit";
    }

    if (
      matchesImportLabelHint(label, [
        "debiteur",
        "débiteur",
        "a recevoir",
        "à recevoir",
        "deductible",
        "déductible",
        "avance",
        "creance",
        "créance",
      ])
    ) {
      return "debit";
    }

    if (
      matchesImportLabelHint(label, [
        "crediteur",
        "créditeur",
        "a payer",
        "à payer",
        "dettes",
        "fournisseur",
        "collectee",
        "collectée",
      ])
    ) {
      return "credit";
    }
  }

  if (parentContext?.balance_normal_side) {
    return parentContext.balance_normal_side;
  }

  return inferBalanceSide({
    uiType,
    classCode,
    existingSide: null,
  });
};

const fetchAccountsSnapshot = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT
        a.id,
        a.company_id,
        a.code,
        a.label,
        a.account_type,
        a.parent_id,
        a.is_movable,
        a.is_auxiliary,
        a.is_active,
        a.balance_normal_side,
        a.created_at,
        COALESCE(ac.label, '') AS class_label,
        EXISTS (
          SELECT 1
          FROM entry_lines el
          WHERE el.account_id = a.id
          LIMIT 1
        ) AS has_entries,
        COALESCE((
          SELECT
            SUM(
              CASE
                WHEN je.status = 'validated'
                  THEN scoped_lines.debit - scoped_lines.credit
                ELSE 0
              END
            )
          FROM entry_lines scoped_lines
          JOIN journal_entries je
            ON je.id = scoped_lines.entry_id
          WHERE scoped_lines.account_id = a.id
        ), 0)::numeric AS current_balance
      FROM accounts a
      LEFT JOIN account_classes ac
        ON ac.company_id = a.company_id
        AND ac.code = LEFT(a.code, 1)
      WHERE a.company_id = $1
      ORDER BY a.code ASC
    `,
    [companyId],
  );

  return rows.map((row) => {
    const classCode = cleanString(row.code).charAt(0);
    const statementType = getStatementType(classCode);
    const uiType = inferUiTypeFromStored({
      accountType: row.account_type,
      classCode,
      balanceNormalSide: row.balance_normal_side,
    });

    return {
      id: Number(row.id),
      company_id: Number(row.company_id),
      code: row.code,
      label: row.label,
      account_type: row.account_type,
      parent_id: row.parent_id ? Number(row.parent_id) : null,
      is_movable: row.is_movable,
      is_auxiliary: row.is_auxiliary,
      is_active: row.is_active,
      balance_normal_side: row.balance_normal_side,
      created_at: row.created_at,
      has_entries: Boolean(row.has_entries),
      current_balance: Number(row.current_balance || 0),
      class_code: classCode,
      class_label: row.class_label || ACCOUNT_CLASS_LABELS[classCode] || "",
      statement_type: statementType,
      statement_type_label: TYPE_LABELS[statementType],
      ui_type: uiType,
      ui_type_label: TYPE_LABELS[uiType] || TYPE_LABELS.asset,
      balance_normal_side_label: SIDE_LABELS[row.balance_normal_side] || row.balance_normal_side,
      third_party_type: inferThirdPartyType({
        code: row.code,
        balanceNormalSide: row.balance_normal_side,
        isAuxiliary: row.is_auxiliary,
      }),
    };
  });
};

const resolveTreeMetadata = (accounts) => {
  const accountsById = new Map(accounts.map((account) => [account.id, account]));
  const accountsByCode = new Map(accounts.map((account) => [account.code, account]));
  const resolvedAccounts = accounts.map((account) => {
    let resolvedParent = null;

    if (account.parent_id && accountsById.has(account.parent_id)) {
      resolvedParent = accountsById.get(account.parent_id);
    } else {
      for (let index = account.code.length - 1; index > 0; index -= 1) {
        const candidateCode = account.code.slice(0, index);

        if (accountsByCode.has(candidateCode)) {
          resolvedParent = accountsByCode.get(candidateCode);
          break;
        }
      }
    }

    return {
      ...account,
      resolved_parent_id: resolvedParent?.id || null,
      resolved_parent_code: resolvedParent?.code || null,
      resolved_parent_label: resolvedParent?.label || null,
      children: [],
      child_count: 0,
      has_children: false,
      depth: 0,
    };
  });

  const byId = new Map(resolvedAccounts.map((account) => [account.id, account]));

  resolvedAccounts.forEach((account) => {
    if (!account.resolved_parent_id) {
      return;
    }

    const parentAccount = byId.get(account.resolved_parent_id);

    if (!parentAccount) {
      return;
    }

    parentAccount.children.push(account.id);
  });

  const applyDepth = (account, depth = 0) => {
    account.depth = depth;
    account.child_count = account.children.length;
    account.has_children = account.child_count > 0;

    account.children
      .map((childId) => byId.get(childId))
      .filter(Boolean)
      .sort((left, right) => left.code.localeCompare(right.code))
      .forEach((childAccount) => applyDepth(childAccount, depth + 1));
  };

  resolvedAccounts
    .filter((account) => !account.resolved_parent_id)
    .sort((left, right) => left.code.localeCompare(right.code))
    .forEach((account) => applyDepth(account));

  return resolvedAccounts.sort((left, right) => left.code.localeCompare(right.code));
};

const normalizeQueryTypeFilter = (value) => {
  const normalized = cleanString(value).toLowerCase();

  if (!normalized) {
    return "";
  }

  if (["bilan", "balance"].includes(normalized)) {
    return "bilan";
  }

  if (["gestion", "income"].includes(normalized)) {
    return "gestion";
  }

  return "";
};

const filterAccountsForList = (accounts, query) => {
  const search = cleanString(query.search).toLowerCase();
  const classFilter = cleanString(query.class).replace(/[^1-7]/g, "");
  const typeFilter = normalizeQueryTypeFilter(query.type);
  const activeFilter = cleanString(query.active).toLowerCase();

  const matchedIds = new Set();
  const accountsById = new Map(accounts.map((account) => [account.id, account]));

  accounts.forEach((account) => {
    const matchesSearch =
      !search ||
      account.code.toLowerCase().includes(search) ||
      account.label.toLowerCase().includes(search);
    const matchesClass = !classFilter || account.class_code === classFilter;
    const matchesType = !typeFilter || account.statement_type === typeFilter;
    const matchesActive =
      activeFilter === "all"
        ? true
        : ["false", "inactive", "0"].includes(activeFilter)
          ? !account.is_active
          : account.is_active;

    if (matchesSearch && matchesClass && matchesType && matchesActive) {
      matchedIds.add(account.id);

      let ancestorId = account.resolved_parent_id;

      while (ancestorId) {
        matchedIds.add(ancestorId);
        ancestorId = accountsById.get(ancestorId)?.resolved_parent_id || null;
      }
    }
  });

  return accounts.filter((account) => matchedIds.has(account.id));
};

const sanitizeAccountForAudit = (account) => ({
  id: account.id,
  code: account.code,
  label: account.label,
  account_type: account.account_type,
  parent_id: account.parent_id,
  is_movable: account.is_movable,
  is_auxiliary: account.is_auxiliary,
  is_active: account.is_active,
  balance_normal_side: account.balance_normal_side,
});

const findPrefixParent = ({ accounts, code, currentId = null }) => {
  for (let index = code.length - 1; index > 0; index -= 1) {
    const candidateCode = code.slice(0, index);
    const candidate = accounts.find(
      (account) =>
        Number(account.id) !== Number(currentId) && account.code === candidateCode,
    );

    if (candidate) {
      return candidate;
    }
  }

  return null;
};

const ensureUniqueCode = ({ accounts, code, currentId = null }) => {
  const duplicate = accounts.find(
    (account) => account.code === code && Number(account.id) !== Number(currentId),
  );

  if (duplicate) {
    throw new ApiError(409, "Ce code compte existe déjà pour cette société.");
  }
};

const ensureParentRules = ({ accounts, currentId = null, code, parentId }) => {
  let parentAccount = null;

  if (!parentId) {
    parentAccount = findPrefixParent({ accounts, code, currentId });
  } else {
    parentAccount =
      accounts.find((account) => Number(account.id) === Number(parentId)) || null;

    if (!parentAccount) {
      throw new ApiError(400, "Le compte parent sélectionné est introuvable.");
    }

    if (Number(parentAccount.id) === Number(currentId)) {
      throw new ApiError(400, "Un compte ne peut pas être son propre parent.");
    }

    if (!code.startsWith(parentAccount.code) || code.length <= parentAccount.code.length) {
      throw new ApiError(
        400,
        "Le code du compte enfant doit prolonger le code du compte parent.",
      );
    }
  }

  if (parentAccount?.is_movable) {
    throw new ApiError(
      400,
      "Le compte parent doit être non mouvementable avant d'accueillir des sous-comptes.",
    );
  }

  return parentAccount;
};

const ensureMovableRules = ({ accounts, currentId = null, code, isMovable }) => {
  if (!isMovable) {
    return;
  }

  const hasChildren = accounts.some(
    (account) =>
      Number(account.id) !== Number(currentId) &&
      account.code.startsWith(code) &&
      account.code.length > code.length,
  );

  if (hasChildren) {
    throw new ApiError(
      400,
      "Un compte parent qui possède des sous-comptes ne peut pas être mouvementable.",
    );
  }
};

const buildAccountPayload = ({ body, existingAccount = null, accounts }) => {
  const code = cleanString(body.code || existingAccount?.code);
  const label = cleanString(body.label || existingAccount?.label);

  if (!ACCOUNT_CODE_REGEX.test(code)) {
    throw new ApiError(400, "Le code compte doit commencer par une classe SCF de 1 à 7.");
  }

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

  const classCode = code.charAt(0);
  const uiType =
    normalizeUiType(body.type) ||
    normalizeUiType(body.ui_type) ||
    normalizeUiType(body.account_type) ||
    inferUiTypeFromStored({
      accountType: existingAccount?.account_type || "",
      classCode,
      balanceNormalSide: existingAccount?.balance_normal_side || null,
    });
  const balanceNormalSide = inferBalanceSide({
    uiType,
    classCode,
    existingSide:
      normalizeBalanceSide(body.balance_normal_side) ||
      normalizeBalanceSide(body.balanceNormalSide) ||
      existingAccount?.balance_normal_side ||
      null,
  });
  const isMovable = parseBoolean(body.is_movable, existingAccount?.is_movable ?? true);
  const isAuxiliary = parseBoolean(body.is_auxiliary, existingAccount?.is_auxiliary ?? false);
  const isActive = parseBoolean(body.is_active, existingAccount?.is_active ?? true);
  const parentIdRaw =
    body.parent_id === "" || body.parent_id === null || body.parent_id === undefined
      ? existingAccount?.parent_id ?? null
      : Number(body.parent_id);

  const payload = {
    code,
    label,
    account_type: mapUiTypeToStoredType({
      uiType,
      classCode,
      code,
      balanceNormalSide,
    }),
    parent_id: Number.isFinite(parentIdRaw) && parentIdRaw > 0 ? parentIdRaw : null,
    is_movable: isMovable,
    is_auxiliary: isAuxiliary,
    is_active: isActive,
    balance_normal_side: balanceNormalSide,
    ui_type: uiType,
    third_party_type:
      normalizeThirdPartyType(body.third_party_type) ||
      normalizeThirdPartyType(body.thirdPartyType) ||
      inferThirdPartyType({
        code,
        balanceNormalSide,
        isAuxiliary,
      }),
  };

  ensureUniqueCode({
    accounts,
    code: payload.code,
    currentId: existingAccount?.id || null,
  });
  ensureParentRules({
    accounts,
    currentId: existingAccount?.id || null,
    code: payload.code,
    parentId: payload.parent_id,
  });
  ensureMovableRules({
    accounts,
    currentId: existingAccount?.id || null,
    code: payload.code,
    isMovable: payload.is_movable,
  });

  return payload;
};

const getAccountById = (accounts, id) =>
  accounts.find((account) => Number(account.id) === Number(id)) || null;

const createOrUpdateAccount = async (db, { companyId, accountId = null, payload }) => {
  if (accountId) {
    const { rows } = await db.query(
      `
        UPDATE accounts
        SET
          code = $2,
          label = $3,
          account_type = $4,
          parent_id = $5,
          is_movable = $6,
          is_auxiliary = $7,
          is_active = $8,
          balance_normal_side = $9
        WHERE id = $1
          AND company_id = $10
        RETURNING *
      `,
      [
        accountId,
        payload.code,
        payload.label,
        payload.account_type,
        payload.parent_id,
        payload.is_movable,
        payload.is_auxiliary,
        payload.is_active,
        payload.balance_normal_side,
        companyId,
      ],
    );

    return rows[0] || null;
  }

  const { rows } = await db.query(
    `
      INSERT INTO accounts (
        company_id,
        code,
        label,
        account_type,
        parent_id,
        is_movable,
        is_auxiliary,
        is_active,
        balance_normal_side
      )
      VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
      RETURNING *
    `,
    [
      companyId,
      payload.code,
      payload.label,
      payload.account_type,
      payload.parent_id,
      payload.is_movable,
      payload.is_auxiliary,
      payload.is_active,
      payload.balance_normal_side,
    ],
  );

  return rows[0] || null;
};

const buildImportRowPreview = ({
  row,
  rowNumber,
  mapping,
  existingAccountsByCode,
  batchCodes,
  previewByCode,
}) => {
  const errors = [];
  const warnings = [];
  const code = cleanString(row[mapping.code]).replace(/\s+/g, "");
  const label = cleanString(row[mapping.label]);
  const rawType = mapping.type ? cleanString(row[mapping.type]) : "";
  const parentCode =
    (mapping.parent_code ? cleanString(row[mapping.parent_code]) : "") ||
    findClosestPrefixCode({
      code,
      batchCodes,
      existingAccountsByCode,
    }) ||
    "";

  if (!code) {
    errors.push("Code manquant.");
  } else if (!ACCOUNT_CODE_REGEX.test(code)) {
    errors.push("Code SCF invalide.");
  }

  if (!label) {
    errors.push("Libellé manquant.");
  }

  const classCode = code.charAt(0);
  const existingContext = getImportContextFromAccount(existingAccountsByCode.get(code));
  const parentPreview = parentCode ? previewByCode.get(parentCode) || null : null;
  const parentContext = parentPreview
    ? {
        ui_type: parentPreview.normalized.ui_type,
        balance_normal_side: parentPreview.normalized.balance_normal_side,
      }
    : getImportContextFromAccount(parentCode ? existingAccountsByCode.get(parentCode) : null);
  const uiType =
    normalizeUiType(rawType) ||
    inferImportUiType({
      code,
      label,
      classCode,
      existingContext,
      parentContext,
    });

  if (!uiType) {
    warnings.push("Type non fourni, valeur inférée automatiquement.");
  }

  if (parentCode && !ACCOUNT_CODE_REGEX.test(parentCode)) {
    errors.push("Code parent invalide.");
  }

  if (parentCode && !batchCodes.has(parentCode) && !existingAccountsByCode.has(parentCode)) {
    errors.push("Le compte parent n'existe pas dans le fichier ou la base.");
  }

  if (parentCode && code && (!code.startsWith(parentCode) || code.length <= parentCode.length)) {
    errors.push("Le code parent doit être un préfixe du code enfant.");
  }

  const balanceNormalSide = inferImportBalanceSide({
    code,
    label,
    classCode,
    uiType: uiType || "asset",
    existingContext,
    parentContext,
  });
  const isAuxiliary =
    existingAccountsByCode.get(code)?.is_auxiliary ||
    (classCode === "4" && code.length >= 3 && /^(40|41|46)/.test(code));
  const thirdPartyType = inferThirdPartyType({
    code,
    balanceNormalSide,
    isAuxiliary,
  });

  return {
    row_number: rowNumber,
    original: row,
    normalized: {
      code,
      label,
      class_code: classCode,
      class_label: ACCOUNT_CLASS_LABELS[classCode] || "",
      statement_type: getStatementType(classCode),
      statement_type_label: TYPE_LABELS[getStatementType(classCode)],
      ui_type: uiType || "asset",
      ui_type_label: TYPE_LABELS[uiType || "asset"],
      parent_code: parentCode || null,
      balance_normal_side: balanceNormalSide,
      balance_normal_side_label: SIDE_LABELS[balanceNormalSide],
      is_auxiliary: isAuxiliary,
      third_party_type: thirdPartyType,
      action: existingAccountsByCode.has(code) ? "update" : "create",
    },
    warnings,
    errors,
  };
};

const buildImportPreview = ({ rows, mapping, existingAccounts }) => {
  if (!mapping.code || !mapping.label) {
    throw new ApiError(400, "Les colonnes code et libellé doivent être mappées.");
  }

  const existingAccountsByCode = new Map(existingAccounts.map((account) => [account.code, account]));
  const batchCodes = new Set(rows.map((row) => cleanString(row[mapping.code]).replace(/\s+/g, "")));
  const previewByCode = new Map();
  const previewRows = rows.map((row, index) => {
    const previewRow = buildImportRowPreview({
      row,
      rowNumber: index + 2,
      mapping,
      existingAccountsByCode,
      batchCodes,
      previewByCode,
    });

    if (previewRow.normalized.code) {
      previewByCode.set(previewRow.normalized.code, previewRow);
    }

    return previewRow;
  });

  const validRows = previewRows.filter((row) => row.errors.length === 0);

  return {
    summary: {
      total_rows: rows.length,
      valid_rows: validRows.length,
      error_rows: previewRows.length - validRows.length,
      warning_rows: previewRows.filter((row) => row.warnings.length > 0).length,
    },
    rows: previewRows,
  };
};

const commitImport = async ({
  db,
  companyId,
  userId,
  ipAddress,
  previewRows,
}) => {
  const orderedRows = previewRows
    .filter((row) => row.errors.length === 0)
    .sort(
      (left, right) =>
        left.normalized.code.length - right.normalized.code.length ||
        left.normalized.code.localeCompare(right.normalized.code),
    );
  const validCodes = orderedRows.map((row) => row.normalized.code);

  const logs = {
    successes: [],
    warnings: [],
    errors: previewRows
      .filter((row) => row.errors.length > 0)
      .map((row) => ({
        row_number: row.row_number,
        code: row.normalized.code,
        messages: row.errors,
      })),
  };
  let createdCount = 0;
  let updatedCount = 0;

  for (const row of orderedRows) {
    const accounts = await fetchAccountsSnapshot(db, companyId);
    const existingAccount = accounts.find((account) => account.code === row.normalized.code) || null;
    const parentAccount = row.normalized.parent_code
      ? accounts.find((account) => account.code === row.normalized.parent_code) || null
      : null;
    const payload = buildAccountPayload({
      body: {
        code: row.normalized.code,
        label: row.normalized.label,
        type: row.normalized.ui_type,
        parent_id: parentAccount?.id || null,
        balance_normal_side: row.normalized.balance_normal_side,
        is_movable:
          !accounts.some(
            (account) =>
              account.code.startsWith(row.normalized.code) &&
              account.code.length > row.normalized.code.length,
          ) &&
          !validCodes.some(
            (code) =>
              code !== row.normalized.code &&
              code.startsWith(row.normalized.code) &&
              code.length > row.normalized.code.length,
          ),
        is_auxiliary: row.normalized.is_auxiliary,
        is_active: true,
        third_party_type: row.normalized.third_party_type,
      },
      existingAccount,
      accounts,
    });
    const savedAccount = await createOrUpdateAccount(db, {
      companyId,
      accountId: existingAccount?.id || null,
      payload,
    });

    await logAudit(db, {
      companyId,
      userId,
      action: existingAccount ? "accounts.import_update" : "accounts.import_create",
      entityType: "account",
      entityId: savedAccount.id,
      oldData: existingAccount ? sanitizeAccountForAudit(existingAccount) : null,
      newData: sanitizeAccountForAudit(savedAccount),
      ipAddress,
    });

    if (existingAccount) {
      updatedCount += 1;
    } else {
      createdCount += 1;
    }

    logs.successes.push({
      row_number: row.row_number,
      code: row.normalized.code,
      action: existingAccount ? "updated" : "created",
    });

    if (row.warnings.length) {
      logs.warnings.push({
        row_number: row.row_number,
        code: row.normalized.code,
        messages: row.warnings,
      });
    }
  }

  await logAudit(db, {
    companyId,
    userId,
    action: "accounts.import",
    entityType: "account_import",
    newData: {
      created_count: createdCount,
      updated_count: updatedCount,
      error_count: logs.errors.length,
      warning_count: logs.warnings.length,
    },
    ipAddress,
  });

  return {
    created_count: createdCount,
    updated_count: updatedCount,
    error_count: logs.errors.length,
    warning_count: logs.warnings.length,
    logs,
  };
};

export const accountsController = {
  list: async (req, res) => {
    const companyId = getRequestCompanyId(req);
    const accounts = resolveTreeMetadata(await fetchAccountsSnapshot(pool, companyId));
    const visibleAccounts = filterAccountsForList(accounts, req.query);

    res.json({
      company_id: companyId,
      items: visibleAccounts.map((account) => ({
        ...account,
        movement_normal_label: SIDE_LABELS[account.balance_normal_side] || account.balance_normal_side,
        is_movable_label: BOOLEAN_LABELS[String(account.is_movable)],
        is_auxiliary_label: BOOLEAN_LABELS[String(account.is_auxiliary)],
        is_active_label: account.is_active ? "Actif" : "Inactif",
      })),
      meta: {
        total_count: accounts.length,
        visible_count: visibleAccounts.length,
        parent_options: accounts
          .filter((account) => account.is_active)
          .map((account) => ({
            id: account.id,
            code: account.code,
            label: account.label,
            class_code: account.class_code,
          })),
      },
    });
  },

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

    if (!accountId) {
      throw new ApiError(400, "Identifiant de compte invalide.");
    }

    const accounts = resolveTreeMetadata(await fetchAccountsSnapshot(pool, companyId));
    const account = getAccountById(accounts, accountId);

    if (!account) {
      throw new ApiError(404, "Compte introuvable.");
    }

    res.json({
      item: account,
    });
  },

  listAuxiliaries: async (req, res) => {
    const companyId = getRequestCompanyId(req);
    const accountId = Number(req.query.account_id || 0) || null;
    const search = cleanString(req.query.search).toLowerCase();
    const values = [companyId];
    const whereClauses = ["aa.company_id = $1"];

    if (accountId) {
      values.push(accountId);
      whereClauses.push(`aa.account_id = $${values.length}`);
    }

    if (search) {
      values.push(`%${search}%`);
      whereClauses.push(
        `(aa.ref_code ILIKE $${values.length} OR aa.name ILIKE $${values.length})`,
      );
    }

    const { rows } = await pool.query(
      `
        SELECT
          aa.id,
          aa.account_id,
          aa.third_party_type,
          aa.ref_code,
          aa.name,
          aa.email,
          aa.phone,
          a.code AS account_code,
          a.label AS account_label,
          COALESCE(
            SUM(
              CASE
                WHEN je.status = 'validated'
                  THEN el.debit - el.credit
                ELSE 0
              END
            ),
            0
          )::numeric AS current_balance
        FROM auxiliary_accounts aa
        JOIN accounts a
          ON a.id = aa.account_id
        LEFT JOIN entry_lines el
          ON el.auxiliary_account_id = aa.id
        LEFT JOIN journal_entries je
          ON je.id = el.entry_id
        WHERE ${whereClauses.join("\nAND ")}
        GROUP BY aa.id, a.code, a.label
        ORDER BY aa.ref_code ASC, aa.name ASC
        LIMIT 100
      `,
      values,
    );

    res.json({
      company_id: companyId,
      items: rows.map((row) => ({
        id: Number(row.id),
        account_id: Number(row.account_id),
        third_party_type: row.third_party_type,
        ref_code: row.ref_code,
        name: row.name,
        email: row.email,
        phone: row.phone,
        account_code: row.account_code,
        account_label: row.account_label,
        current_balance: Number(row.current_balance || 0),
      })),
    });
  },

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

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

      const accounts = await fetchAccountsSnapshot(client, companyId);
      const payload = buildAccountPayload({
        body: req.body,
        accounts,
      });
      const savedAccount = await createOrUpdateAccount(client, {
        companyId,
        payload,
      });

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "accounts.create",
        entityType: "account",
        entityId: savedAccount.id,
        oldData: null,
        newData: sanitizeAccountForAudit(savedAccount),
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.status(201).json({
        message: "Compte créé avec succès.",
        item: savedAccount,
      });
    } catch (error) {
      await client.query("ROLLBACK");

      if (error?.code === "23505") {
        throw new ApiError(409, "Ce code compte existe déjà pour cette société.");
      }

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

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

    if (!accountId) {
      throw new ApiError(400, "Identifiant de compte invalide.");
    }

    const client = await pool.connect();

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

      const accounts = await fetchAccountsSnapshot(client, companyId);
      const existingAccount = getAccountById(accounts, accountId);

      if (!existingAccount) {
        throw new ApiError(404, "Compte introuvable.");
      }

      const payload = buildAccountPayload({
        body: req.body,
        existingAccount,
        accounts,
      });
      const savedAccount = await createOrUpdateAccount(client, {
        companyId,
        accountId,
        payload,
      });

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "accounts.update",
        entityType: "account",
        entityId: savedAccount.id,
        oldData: sanitizeAccountForAudit(existingAccount),
        newData: sanitizeAccountForAudit(savedAccount),
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.json({
        message: "Compte mis à jour avec succès.",
        item: savedAccount,
      });
    } catch (error) {
      await client.query("ROLLBACK");

      if (error?.code === "23505") {
        throw new ApiError(409, "Ce code compte existe déjà pour cette société.");
      }

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

  remove: async (req, res) => {
    const companyId = getRequestCompanyId(req, req.query);
    const accountId = Number(req.params.id);

    if (!accountId) {
      throw new ApiError(400, "Identifiant de compte invalide.");
    }

    const client = await pool.connect();

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

      const accounts = await fetchAccountsSnapshot(client, companyId);
      const existingAccount = getAccountById(accounts, accountId);

      if (!existingAccount) {
        throw new ApiError(404, "Compte introuvable.");
      }

      if (existingAccount.has_entries) {
        throw new ApiError(
          409,
          "Ce compte ne peut pas être supprimé car il est déjà utilisé dans des écritures.",
        );
      }

      const { rows } = await client.query(
        `
          UPDATE accounts
          SET is_active = FALSE
          WHERE id = $1
            AND company_id = $2
          RETURNING *
        `,
        [accountId, companyId],
      );

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "accounts.soft_delete",
        entityType: "account",
        entityId: accountId,
        oldData: sanitizeAccountForAudit(existingAccount),
        newData: sanitizeAccountForAudit(rows[0]),
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.json({
        message: "Compte désactivé avec succès.",
        item: rows[0],
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

  importAccounts: async (req, res) => {
    const companyId = getRequestCompanyId(req, req.body);
    const mode = parseImportMode(req.body.mode);
    const mapping = parseImportMapping(req.body.mapping);
    const { columns, rows } = readSpreadsheet(req.file);
    const existingAccounts = await fetchAccountsSnapshot(pool, companyId);

    if (mode === "inspect") {
      return res.json({
        mode,
        columns,
        suggested_mapping: detectSuggestedMapping(columns),
        preview_rows: rows.slice(0, MAX_IMPORT_PREVIEW_ROWS),
        summary: {
          total_rows: rows.length,
        },
      });
    }

    const preview = buildImportPreview({
      rows,
      mapping,
      existingAccounts,
    });

    if (mode === "preview") {
      return res.json({
        mode,
        columns,
        mapping,
        suggested_mapping: detectSuggestedMapping(columns),
        summary: preview.summary,
        preview_rows: preview.rows.slice(0, MAX_IMPORT_PREVIEW_ROWS),
      });
    }

    const client = await pool.connect();

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

      const importResult = await commitImport({
        db: client,
        companyId,
        userId: req.user?.sub || null,
        ipAddress: req.ip,
        previewRows: preview.rows,
      });

      await client.query("COMMIT");

      return res.json({
        message: "Import du plan comptable terminé.",
        mode,
        summary: preview.summary,
        ...importResult,
      });
    } catch (error) {
      await client.query("ROLLBACK");

      if (error?.code === "23505") {
        throw new ApiError(409, "Des codes comptes en doublon ont été détectés.");
      }

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