import XLSX from "xlsx";

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

const JOURNAL_TYPE_LABELS = {
  purchase: "Achats",
  sale: "Ventes",
  cash: "Caisse",
  bank: "Banque",
  misc: "OD",
  payroll: "Paie",
  assets: "Immobilisations",
};

const NUMBERING_MODE_LABELS = {
  continuous: "Automatique séquentiel",
  manual: "Manuel",
};

const JOURNAL_TYPE_IMPORT_ALIASES = {
  purchase: ["purchase", "achat", "achats"],
  sale: ["sale", "vente", "ventes"],
  cash: ["cash", "caisse", "especes", "especes_caisse"],
  bank: ["bank", "banque", "bancaire"],
  misc: ["misc", "od", "operations_diverses", "operation_diverse", "divers"],
  payroll: ["payroll", "paie", "salaires"],
  assets: ["assets", "immobilisation", "immobilisations"],
};

const NUMBERING_MODE_IMPORT_ALIASES = {
  continuous: [
    "continuous",
    "automatique_sequentiel",
    "automatique_sequence",
    "sequentiel",
    "sequence",
    "auto",
  ],
  manual: ["manual", "manuel"],
};

const JOURNAL_TEMPLATE_HEADERS = [
  "Code",
  "Libellé",
  "Type",
  "Compte contrepartie",
  "Numérotation",
  "Actif",
];

const JOURNAL_IMPORT_ALIASES = {
  code: ["code", "journal_code"],
  label: ["libelle", "libellé", "label", "intitule", "intitulé"],
  journal_type: ["type", "journal_type", "type_journal"],
  default_counterpart_account: [
    "compte_contrepartie",
    "compte_de_contrepartie",
    "default_counterpart_account",
    "counterpart_account",
    "compte",
  ],
  numbering_mode: ["numerotation", "numérotation", "numbering_mode", "mode_numerotation"],
  is_active: ["actif", "is_active", "active", "statut"],
};

const MAX_IMPORT_PREVIEW_ROWS = 20;
const allowedJournalTypes = Object.keys(JOURNAL_TYPE_LABELS);
const allowedNumberingModes = Object.keys(NUMBERING_MODE_LABELS);

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

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

const parseDelimitedLine = (line, delimiter) => {
  const cells = [];
  let current = "";
  let inQuotes = false;

  for (let index = 0; index < line.length; index += 1) {
    const character = line[index];
    const nextCharacter = line[index + 1];

    if (character === '"') {
      if (inQuotes && nextCharacter === '"') {
        current += '"';
        index += 1;
      } else {
        inQuotes = !inQuotes;
      }

      continue;
    }

    if (character === delimiter && !inQuotes) {
      cells.push(current);
      current = "";
      continue;
    }

    current += character;
  }

  cells.push(current);

  return cells;
};

const readImportCsv = (file) => {
  const textContent = file.buffer.toString("utf8").replace(/^\uFEFF/, "");
  const lines = textContent
    .split(/\r?\n/)
    .map((line) => line.replace(/\r$/, ""))
    .filter((line) => line.trim().length > 0);

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

  const headerLine = lines[0];
  const delimiter = headerLine.includes(";") ? ";" : ",";
  const headers = parseDelimitedLine(headerLine, delimiter).map(
    (column, index) => cleanString(column) || `column_${index + 1}`,
  );
  const rows = lines.slice(1).map((line) => {
    const values = parseDelimitedLine(line, delimiter);

    return headers.reduce((accumulator, header, index) => {
      accumulator[header] = values[index] ?? "";
      return accumulator;
    }, {});
  });

  return {
    headers,
    rows,
  };
};

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

  const fileName = cleanString(file.originalname).toLowerCase();

  if (fileName.endsWith(".csv")) {
    return readImportCsv(file);
  }

  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 headers = matrix[0].map((column, index) => cleanString(column) || `column_${index + 1}`);
  const rows = matrix.slice(1).map((rowValues) =>
    headers.reduce((accumulator, header, index) => {
      accumulator[header] = rowValues[index] ?? "";
      return accumulator;
    }, {}),
  );

  return {
    headers,
    rows,
  };
};

const resolveImportField = (row, fieldName) => {
  const aliases = JOURNAL_IMPORT_ALIASES[fieldName] || [];

  for (const [columnName, value] of Object.entries(row)) {
    if (aliases.includes(normalizeImportKey(columnName))) {
      return value;
    }
  }

  return "";
};

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

  const normalized = normalizeImportKey(value);

  if (!normalized) {
    return fallback;
  }

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

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

  return fallback;
};

const resolveImportedOption = (value, aliases, fieldLabel, fallback = null) => {
  const normalized = normalizeImportKey(value);

  if (!normalized) {
    return fallback;
  }

  for (const [option, acceptedValues] of Object.entries(aliases)) {
    if (acceptedValues.includes(normalized)) {
      return option;
    }
  }

  throw new ApiError(400, `${fieldLabel} invalide.`);
};

const buildJournalTemplateBuffer = () => {
  const worksheet = XLSX.utils.json_to_sheet(
    [
      {
        Code: "ACH",
        "Libellé": "Journal des achats",
        Type: "Achats",
        "Compte contrepartie": "401",
        "Numérotation": "Automatique séquentiel",
        Actif: "oui",
      },
    ],
    {
      header: JOURNAL_TEMPLATE_HEADERS,
    },
  );
  const workbook = XLSX.utils.book_new();

  XLSX.utils.book_append_sheet(workbook, worksheet, "Journaux");

  return XLSX.write(workbook, {
    type: "buffer",
    bookType: "xlsx",
  });
};

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

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

  return "preview";
};

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 sanitizeJournal = (journal) => ({
  id: journal.id,
  code: journal.code,
  label: journal.label,
  journal_type: journal.journal_type,
  default_counterpart_account: journal.default_counterpart_account,
  numbering_mode: journal.numbering_mode,
  is_active: journal.is_active,
});

const mapJournalRow = (row) => ({
  id: Number(row.id),
  code: row.code,
  label: row.label,
  journal_type: row.journal_type,
  journal_type_label: JOURNAL_TYPE_LABELS[row.journal_type] || row.journal_type,
  default_counterpart_account: row.default_counterpart_account
    ? Number(row.default_counterpart_account)
    : null,
  counterpart_code: row.counterpart_code,
  counterpart_label: row.counterpart_label,
  numbering_mode: row.numbering_mode,
  numbering_mode_label: NUMBERING_MODE_LABELS[row.numbering_mode] || row.numbering_mode,
  is_active: row.is_active,
});

const listJournalRows = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT
        j.id,
        j.code,
        j.label,
        j.journal_type,
        j.default_counterpart_account,
        j.numbering_mode,
        j.is_active,
        a.code AS counterpart_code,
        a.label AS counterpart_label
      FROM journals j
      LEFT JOIN accounts a
        ON a.id = j.default_counterpart_account
      WHERE j.company_id = $1
      ORDER BY j.code ASC
    `,
    [companyId],
  );

  return rows.map(mapJournalRow);
};

const fetchCounterpartAccount = async (db, companyId, accountId) => {
  if (!accountId) {
    return null;
  }

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

  return rows[0] || null;
};

const fetchCounterpartAccountByCode = async (db, companyId, accountCode) => {
  const code = cleanString(accountCode);

  if (!code) {
    return null;
  }

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

  if (!rows[0]) {
    throw new ApiError(400, "Le compte de contrepartie est introuvable.");
  }

  return rows[0];
};

const normalizeJournalPayload = async ({ db, companyId, body, existingJournal = null }) => {
  const code = cleanString(body.code || existingJournal?.code).toUpperCase();
  const label = cleanString(body.label || existingJournal?.label);
  const journalType = cleanString(body.journal_type || body.type || existingJournal?.journal_type)
    .toLowerCase();
  const numberingMode = cleanString(
    body.numbering_mode || body.numberingMode || existingJournal?.numbering_mode || "continuous",
  ).toLowerCase();
  const defaultCounterpartAccount =
    body.default_counterpart_account === "" || body.default_counterpart_account === null
      ? null
      : Number(
          body.default_counterpart_account ||
            body.defaultCounterpartAccount ||
            existingJournal?.default_counterpart_account ||
            0,
        ) || null;
  const isActive =
    body.is_active === undefined
      ? existingJournal?.is_active ?? true
      : Boolean(body.is_active);

  if (!code || code.length > 5) {
    throw new ApiError(400, "Le code journal est obligatoire et limité à 5 caractères.");
  }

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

  if (!allowedJournalTypes.includes(journalType)) {
    throw new ApiError(400, "Le type de journal est invalide.");
  }

  if (!allowedNumberingModes.includes(numberingMode)) {
    throw new ApiError(400, "Le mode de numérotation est invalide.");
  }

  if (defaultCounterpartAccount) {
    const account = await fetchCounterpartAccount(db, companyId, defaultCounterpartAccount);

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

  return {
    code,
    label,
    journal_type: journalType,
    default_counterpart_account: defaultCounterpartAccount,
    numbering_mode: numberingMode,
    is_active: isActive,
  };
};

const getJournalById = async (db, companyId, journalId) => {
  const { rows } = await db.query(
    `
      SELECT *
      FROM journals
      WHERE id = $1
        AND company_id = $2
      LIMIT 1
    `,
    [journalId, companyId],
  );

  return rows[0] || null;
};

const getJournalByCode = async (db, companyId, code) => {
  const normalizedCode = cleanString(code).toUpperCase();

  if (!normalizedCode) {
    return null;
  }

  const { rows } = await db.query(
    `
      SELECT *
      FROM journals
      WHERE company_id = $1
        AND code = $2
      LIMIT 1
    `,
    [companyId, normalizedCode],
  );

  return rows[0] || null;
};

const getJournalListItemById = async (db, companyId, journalId) => {
  const { rows } = await db.query(
    `
      SELECT
        j.id,
        j.code,
        j.label,
        j.journal_type,
        j.default_counterpart_account,
        j.numbering_mode,
        j.is_active,
        a.code AS counterpart_code,
        a.label AS counterpart_label
      FROM journals j
      LEFT JOIN accounts a
        ON a.id = j.default_counterpart_account
      WHERE j.id = $1
        AND j.company_id = $2
      LIMIT 1
    `,
    [journalId, companyId],
  );

  return rows[0] ? mapJournalRow(rows[0]) : null;
};

const saveJournal = async ({
  db,
  companyId,
  body,
  existingJournal = null,
  userId = null,
  ipAddress = null,
}) => {
  const payload = await normalizeJournalPayload({
    db,
    companyId,
    body,
    existingJournal,
  });
  let journalId = existingJournal?.id || null;

  if (existingJournal) {
    const { rows } = await db.query(
      `
        UPDATE journals
        SET
          code = $2,
          label = $3,
          journal_type = $4,
          default_counterpart_account = $5,
          numbering_mode = $6,
          is_active = $7
        WHERE id = $1
          AND company_id = $8
        RETURNING id
      `,
      [
        existingJournal.id,
        payload.code,
        payload.label,
        payload.journal_type,
        payload.default_counterpart_account,
        payload.numbering_mode,
        payload.is_active,
        companyId,
      ],
    );

    journalId = Number(rows[0].id);
  } else {
    const { rows } = await db.query(
      `
        INSERT INTO journals (
          company_id,
          code,
          label,
          journal_type,
          default_counterpart_account,
          numbering_mode,
          is_active
        )
        VALUES ($1, $2, $3, $4, $5, $6, $7)
        RETURNING id
      `,
      [
        companyId,
        payload.code,
        payload.label,
        payload.journal_type,
        payload.default_counterpart_account,
        payload.numbering_mode,
        payload.is_active,
      ],
    );

    journalId = Number(rows[0].id);
  }

  const item = await getJournalListItemById(db, companyId, journalId);

  await logAudit(db, {
    companyId,
    userId,
    action: existingJournal ? "journals.update" : "journals.create",
    entityType: "journal",
    entityId: journalId,
    oldData: existingJournal ? sanitizeJournal(existingJournal) : null,
    newData: sanitizeJournal(item),
    ipAddress,
  });

  return item;
};

const buildJournalImportPayload = async ({ db, companyId, row, existingJournal = null }) => {
  const counterpartAccount = await fetchCounterpartAccountByCode(
    db,
    companyId,
    resolveImportField(row, "default_counterpart_account"),
  );
  const journalType = resolveImportedOption(
    resolveImportField(row, "journal_type"),
    JOURNAL_TYPE_IMPORT_ALIASES,
    "Le type de journal",
    existingJournal?.journal_type || null,
  );
  const numberingMode = resolveImportedOption(
    resolveImportField(row, "numbering_mode"),
    NUMBERING_MODE_IMPORT_ALIASES,
    "Le mode de numérotation",
    existingJournal?.numbering_mode || "continuous",
  );

  const payload = await normalizeJournalPayload({
    db,
    companyId,
    body: {
      code: resolveImportField(row, "code"),
      label: resolveImportField(row, "label"),
      journal_type: journalType,
      default_counterpart_account: counterpartAccount ? Number(counterpartAccount.id) : null,
      numbering_mode: numberingMode,
      is_active: parseBoolean(
        resolveImportField(row, "is_active"),
        existingJournal?.is_active ?? true,
      ),
    },
    existingJournal,
  });

  return {
    ...payload,
    counterpart_code: counterpartAccount?.code || null,
    counterpart_label: counterpartAccount?.label || null,
  };
};

const buildJournalImportPreview = async ({ db, companyId, rows }) => {
  const previewRows = [];
  const seenCodes = new Set();

  for (const [index, row] of rows.entries()) {
    const rowNumber = index + 2;
    const rawCode = cleanString(resolveImportField(row, "code")).toUpperCase();
    const rawLabel = cleanString(resolveImportField(row, "label"));
    const errors = [];
    let normalized = null;
    let existingJournal = null;

    if (!rawCode) {
      errors.push("Le code journal est obligatoire.");
    }

    if (!rawLabel) {
      errors.push("Le libellé est obligatoire.");
    }

    if (rawCode) {
      if (seenCodes.has(rawCode)) {
        errors.push("Code journal en doublon dans le fichier.");
      } else {
        seenCodes.add(rawCode);
      }
    }

    try {
      existingJournal = rawCode ? await getJournalByCode(db, companyId, rawCode) : null;
      const payload = await buildJournalImportPayload({
        db,
        companyId,
        row,
        existingJournal,
      });

      normalized = {
        code: payload.code,
        label: payload.label,
        journal_type: payload.journal_type,
        journal_type_label: JOURNAL_TYPE_LABELS[payload.journal_type] || payload.journal_type,
        default_counterpart_account: payload.default_counterpart_account,
        counterpart_code: payload.counterpart_code,
        counterpart_label: payload.counterpart_label,
        numbering_mode: payload.numbering_mode,
        numbering_mode_label:
          NUMBERING_MODE_LABELS[payload.numbering_mode] || payload.numbering_mode,
        is_active: payload.is_active,
        action: existingJournal ? "update" : "create",
      };
    } catch (error) {
      errors.push(error.message || "Ligne d'import invalide.");
    }

    previewRows.push({
      row_number: rowNumber,
      original: row,
      normalized:
        normalized || {
          code: rawCode || null,
          label: rawLabel || null,
          counterpart_code: cleanString(resolveImportField(row, "default_counterpart_account")) || null,
          action: existingJournal ? "update" : "create",
        },
      errors,
      warnings: [],
    });
  }

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

const commitJournalImport = async ({
  db,
  companyId,
  rows,
  userId,
  ipAddress,
}) => {
  let createdCount = 0;
  let updatedCount = 0;
  const logs = {
    successes: [],
    errors: [],
  };

  for (const row of rows) {
    if (row.errors.length > 0) {
      logs.errors.push({
        row_number: row.row_number,
        code: row.normalized?.code || null,
        messages: row.errors,
      });
      continue;
    }

    const existingJournal = row.normalized?.code
      ? await getJournalByCode(db, companyId, row.normalized.code)
      : null;
    const item = await saveJournal({
      db,
      companyId,
      body: {
        code: resolveImportField(row.original, "code"),
        label: resolveImportField(row.original, "label"),
        journal_type: row.normalized.journal_type,
        default_counterpart_account: row.normalized.default_counterpart_account,
        numbering_mode: row.normalized.numbering_mode,
        is_active: row.normalized.is_active,
      },
      existingJournal,
      userId,
      ipAddress,
    });

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

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

  await logAudit(db, {
    companyId,
    userId,
    action: "journals.import",
    entityType: "journal_import",
    newData: {
      created_count: createdCount,
      updated_count: updatedCount,
      error_count: logs.errors.length,
      total_rows: rows.length,
    },
    ipAddress,
  });

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

export const journalsController = {
  list: async (req, res) => {
    const companyId = getCompanyId(req);
    const items = await listJournalRows(pool, companyId);

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

  downloadTemplate: async (_req, res) => {
    const workbookBuffer = buildJournalTemplateBuffer();

    res.setHeader(
      "Content-Type",
      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    );
    res.setHeader("Content-Disposition", 'attachment; filename="journals-import-template.xlsx"');
    res.send(workbookBuffer);
  },

  importItems: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    const mode = getImportMode(req.body.mode);
    const { headers, rows } = readImportSpreadsheet(req.file);
    const client = await pool.connect();

    try {
      const preview = await buildJournalImportPreview({
        db: client,
        companyId,
        rows,
      });

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

      await client.query("BEGIN");

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

      await client.query("COMMIT");

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

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

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

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

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

      const item = await saveJournal({
        db: client,
        companyId,
        body: req.body,
        userId: req.user?.sub || null,
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

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

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

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

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

    if (!journalId) {
      throw new ApiError(400, "Identifiant journal invalide.");
    }

    const client = await pool.connect();

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

      const existingJournal = await getJournalById(client, companyId, journalId);

      if (!existingJournal) {
        throw new ApiError(404, "Journal introuvable.");
      }

      const item = await saveJournal({
        db: client,
        companyId,
        body: req.body,
        existingJournal,
        userId: req.user?.sub || null,
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

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

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

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

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

    if (!journalId) {
      throw new ApiError(400, "Identifiant journal invalide.");
    }

    const client = await pool.connect();

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

      const existingJournal = await getJournalById(client, companyId, journalId);

      if (!existingJournal) {
        throw new ApiError(404, "Journal introuvable.");
      }

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

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "journals.soft_delete",
        entityType: "journal",
        entityId: journalId,
        oldData: sanitizeJournal(existingJournal),
        newData: sanitizeJournal(rows[0]),
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

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