import { pool } from "../db/pool.js";
import { ApiError } from "../utils/ApiError.js";
import { logAudit } from "../utils/auditLog.js";
import { markFiscalDeadlineAsFiled } from "../utils/fiscalDeadlines.js";
import { formatDzdAmount } from "../utils/formatters.js";
import {
  buildG50terQuarterMeta,
  calculateIrgBareme,
  deriveFiscalDeclarationYear,
  ensureG50terDeclarations,
  getDeadlineState,
  getQuarterFromDate,
} from "../utils/fiscalG50ter.js";
import { buildG50terPdfBuffer } from "../utils/g50terPdf.js";
import { ADMIN_ROLES } from "../utils/permissions.js";

const TODAY = new Date();

const STATUS_LABELS = {
  brouillon: "Brouillon",
  deposee: "Deposee",
  validee: "Validee",
};

const DEADLINE_STATE_LABELS = {
  upcoming: "A venir",
  due_soon: "Echeance proche",
  overdue: "En retard",
  filed: "Deposee",
};

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

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

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

  return companyId;
};

const parseInteger = (value, label, fallback = 0) => {
  if (value === undefined || value === null || value === "") {
    return fallback;
  }

  const parsed = Number.parseInt(value, 10);

  if (!Number.isFinite(parsed)) {
    throw new ApiError(400, `${label} est invalide.`);
  }

  return parsed;
};

const parseDecimalField = (value, label, fallback = 0) => {
  if (value === undefined || value === null || value === "") {
    return fallback;
  }

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

  if (!Number.isFinite(parsed)) {
    throw new ApiError(400, `${label} est invalide.`);
  }

  if (parsed < 0) {
    throw new ApiError(400, `${label} ne peut pas etre negatif.`);
  }

  return roundAmount(parsed);
};

const parseOptionalIsoDate = (value, label) => {
  const normalized = cleanString(value);

  if (!normalized) {
    return null;
  }

  if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
    throw new ApiError(400, `${label} invalide.`);
  }

  const parsedDate = new Date(`${normalized}T00:00:00Z`);
  const [year, month, day] = normalized.split("-").map(Number);

  if (
    Number.isNaN(parsedDate.getTime()) ||
    parsedDate.getUTCFullYear() !== year ||
    parsedDate.getUTCMonth() + 1 !== month ||
    parsedDate.getUTCDate() !== day
  ) {
    throw new ApiError(400, `${label} invalide.`);
  }

  return normalized;
};

const toNullableText = (value) => {
  const normalized = cleanString(value);
  return normalized || null;
};

const fetchCompany = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT
        id,
        name,
        nif,
        ai,
        address,
        city,
        phone,
        TO_CHAR(registration_date, 'YYYY-MM-DD') AS registration_date
      FROM companies
      WHERE id = $1
      LIMIT 1
    `,
    [companyId],
  );

  if (!rows[0]) {
    throw new ApiError(404, "Societe introuvable.");
  }

  return rows[0];
};

const fetchFiscalProfile = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT
        id,
        nif,
        nin,
        article_imposition,
        code_activite,
        activity_description,
        obligation_g50ter
      FROM fiscal_profiles
      WHERE company_id = $1
      LIMIT 1
    `,
    [companyId],
  );

  return rows[0] || null;
};

const fetchFiscalYears = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT
        id,
        label,
        TO_CHAR(start_date, 'YYYY-MM-DD') AS start_date,
        TO_CHAR(end_date, 'YYYY-MM-DD') AS end_date,
        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: row.start_date,
    end_date: row.end_date,
    is_current: row.is_current,
    annee: deriveFiscalDeclarationYear(row),
  }));
};

const resolveFiscalYearContext = async (db, companyId, options = {}) => {
  const fiscalYears = await fetchFiscalYears(db, companyId);
  const fiscalYearId = Number(options.fiscal_year_id || options.fiscalYearId || 0);
  const requestedYear = Number(options.annee || 0);

  if (!fiscalYears.length) {
    throw new ApiError(404, "Aucun exercice fiscal disponible.");
  }

  if (fiscalYearId) {
    const match = fiscalYears.find((item) => item.id === fiscalYearId);

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

    return match;
  }

  if (requestedYear) {
    const match = fiscalYears.find((item) => Number(item.annee) === requestedYear);

    if (match) {
      return match;
    }
  }

  return fiscalYears.find((item) => item.is_current) || fiscalYears[fiscalYears.length - 1];
};

const G50TER_SELECT = `
  SELECT
    id,
    company_id,
    fiscal_year_id,
    trimestre,
    annee,
    salaires_imposables_t,
    taux_bareme,
    montant_irg,
    nif,
    nin,
    article_imposition,
    code_activite,
    TO_CHAR(date_versement, 'YYYY-MM-DD') AS date_versement,
    TO_CHAR(delai_limite, 'YYYY-MM-DD') AS delai_limite,
    quittance_numero,
    TO_CHAR(quittance_date, 'YYYY-MM-DD') AS quittance_date,
    lieu_declaration,
    TO_CHAR(date_signature, 'YYYY-MM-DD') AS date_signature,
    receveur_signature,
    TO_CHAR(receveur_date, 'YYYY-MM-DD') AS receveur_date,
    statut,
    created_by,
    created_at,
    updated_at
  FROM g50ter_declarations
`;

const normalizeDeclaration = (row) => {
  const meta = buildG50terQuarterMeta(row.annee, row.trimestre);
  const deadlineState = getDeadlineState({
    statut: row.statut,
    deadlineDate: row.delai_limite || meta.deadline_date,
    today: TODAY,
  });

  return {
    id: Number(row.id),
    company_id: Number(row.company_id),
    fiscal_year_id: Number(row.fiscal_year_id),
    trimestre: Number(row.trimestre),
    annee: Number(row.annee),
    quarter_label: meta.label,
    periode_label: meta.periode_label,
    months_label: meta.months_label,
    deadline_label: meta.deadline_label,
    salaires_imposables_t: Number(row.salaires_imposables_t || 0),
    montant_irg: Number(row.montant_irg || 0),
    taux_bareme: row.taux_bareme || "Bareme IRG",
    nif: row.nif || "",
    nin: row.nin || "",
    article_imposition: row.article_imposition || "",
    code_activite: row.code_activite || "",
    date_versement: row.date_versement || "",
    delai_limite: row.delai_limite || meta.deadline_date,
    quittance_numero: row.quittance_numero || "",
    quittance_date: row.quittance_date || "",
    lieu_declaration: row.lieu_declaration || "",
    date_signature: row.date_signature || "",
    receveur_signature: row.receveur_signature || "",
    receveur_date: row.receveur_date || "",
    statut: row.statut || "brouillon",
    statut_label: STATUS_LABELS[row.statut] || row.statut,
    deadline_state: deadlineState,
    deadline_state_label: DEADLINE_STATE_LABELS[deadlineState] || deadlineState,
    formatted_salaires_imposables_t: formatDzdAmount(row.salaires_imposables_t || 0),
    formatted_montant_irg: formatDzdAmount(row.montant_irg || 0),
    created_by: row.created_by ? Number(row.created_by) : null,
    created_at: row.created_at,
    updated_at: row.updated_at,
  };
};

const fetchYearDeclarations = async (db, companyId, annee) => {
  const { rows } = await db.query(
    `${G50TER_SELECT}
      WHERE company_id = $1
        AND annee = $2
      ORDER BY trimestre ASC`,
    [companyId, annee],
  );

  return rows.map(normalizeDeclaration);
};

const fetchDeclarationById = async (db, companyId, declarationId) => {
  const { rows } = await db.query(
    `${G50TER_SELECT}
      WHERE company_id = $1
        AND id = $2
      LIMIT 1`,
    [companyId, declarationId],
  );

  if (!rows[0]) {
    throw new ApiError(404, "Declaration G50ter introuvable.");
  }

  return normalizeDeclaration(rows[0]);
};

const buildDeclarationPayload = ({
  body,
  companyId,
  company,
  profile,
  fiscalYear,
  user,
  existingItem = null,
}) => {
  const annee = deriveFiscalDeclarationYear(fiscalYear);
  const trimestre = parseInteger(
    body.trimestre,
    "Le trimestre",
    existingItem?.trimestre || getQuarterFromDate(),
  );

  if (trimestre < 1 || trimestre > 4) {
    throw new ApiError(400, "Le trimestre doit etre compris entre 1 et 4.");
  }

  const quarterMeta = buildG50terQuarterMeta(annee, trimestre);
  const isAdmin = ADMIN_ROLES.has(user?.role);
  const quittanceDate = parseOptionalIsoDate(body.quittance_date, "La date de quittance");
  const dateSignature = parseOptionalIsoDate(body.date_signature, "La date de signature");
  const receveurDate = parseOptionalIsoDate(body.receveur_date, "La date du receveur");

  return {
    company_id: companyId,
    fiscal_year_id: fiscalYear.id,
    annee,
    trimestre,
    salaires_imposables_t: parseDecimalField(
      body.salaires_imposables_t,
      "Les salaires imposables",
      0,
    ),
    taux_bareme: cleanString(body.taux_bareme) || "Bareme IRG",
    montant_irg: parseDecimalField(body.montant_irg, "Le montant IRG", 0),
    nif: cleanString(profile?.nif || company.nif),
    nin: cleanString(profile?.nin),
    article_imposition: cleanString(profile?.article_imposition || company.ai),
    code_activite: cleanString(profile?.code_activite),
    date_versement: parseOptionalIsoDate(
      body.date_versement || quittanceDate,
      "La date de versement",
    ),
    delai_limite: quarterMeta.deadline_date,
    quittance_numero: toNullableText(body.quittance_numero),
    quittance_date: quittanceDate,
    lieu_declaration: toNullableText(body.lieu_declaration || company.city),
    date_signature: dateSignature,
    receveur_signature: isAdmin
      ? toNullableText(body.receveur_signature)
      : existingItem?.receveur_signature || null,
    receveur_date: isAdmin ? receveurDate : existingItem?.receveur_date || null,
  };
};

export const fiscalG50terController = {
  list: async (req, res) => {
    const companyId = getCompanyId(req);
    const client = await pool.connect();

    try {
      const fiscalYears = await fetchFiscalYears(client, companyId);

      for (const fiscalYear of fiscalYears) {
        await ensureG50terDeclarations(client, {
          companyId,
          fiscalYear,
          userId: req.user?.sub || null,
        });
      }

      const requestedYear = Number(req.query.annee || 0);
      const params = [companyId];
      let whereClause = "WHERE company_id = $1";

      if (requestedYear) {
        params.push(requestedYear);
        whereClause += " AND annee = $2";
      }

      const { rows } = await client.query(
        `${G50TER_SELECT}
          ${whereClause}
          ORDER BY annee DESC, trimestre ASC`,
        params,
      );

      res.json({
        company_id: companyId,
        items: rows.map(normalizeDeclaration),
        meta: {
          current_annee: new Date().getUTCFullYear(),
          current_trimestre: getQuarterFromDate(TODAY),
          fiscal_years: fiscalYears,
        },
      });
    } finally {
      client.release();
    }
  },

  getById: async (req, res) => {
    const companyId = getCompanyId(req);
    const declarationId = parseInteger(req.params.id, "Identifiant de declaration");
    const client = await pool.connect();

    try {
      const item = await fetchDeclarationById(client, companyId, declarationId);
      const fiscalYear = await resolveFiscalYearContext(client, companyId, {
        annee: item.annee,
        fiscal_year_id: item.fiscal_year_id,
      });

      await ensureG50terDeclarations(client, {
        companyId,
        fiscalYear,
        userId: req.user?.sub || null,
      });

      const yearItems = await fetchYearDeclarations(client, companyId, item.annee);

      res.json({
        company_id: companyId,
        item,
        meta: {
          year_items: yearItems,
          current_trimestre: getQuarterFromDate(TODAY),
        },
      });
    } finally {
      client.release();
    }
  },

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

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

      const [company, profile, fiscalYear] = await Promise.all([
        fetchCompany(client, companyId),
        fetchFiscalProfile(client, companyId),
        resolveFiscalYearContext(client, companyId, req.body),
      ]);
      await ensureG50terDeclarations(client, {
        companyId,
        fiscalYear,
        userId: req.user?.sub || null,
      });

      const payload = buildDeclarationPayload({
        body: req.body,
        companyId,
        company,
        profile,
        fiscalYear,
        user: req.user,
      });

      const { rows } = await client.query(
        `
          INSERT INTO g50ter_declarations (
            company_id,
            fiscal_year_id,
            trimestre,
            annee,
            salaires_imposables_t,
            taux_bareme,
            montant_irg,
            nif,
            nin,
            article_imposition,
            code_activite,
            date_versement,
            delai_limite,
            quittance_numero,
            quittance_date,
            lieu_declaration,
            date_signature,
            receveur_signature,
            receveur_date,
            statut,
            created_by
          )
          VALUES (
            $1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
            $11, $12, $13, $14, $15, $16, $17, $18, $19, 'brouillon', $20
          )
          RETURNING id
        `,
        [
          payload.company_id,
          payload.fiscal_year_id,
          payload.trimestre,
          payload.annee,
          payload.salaires_imposables_t,
          payload.taux_bareme,
          payload.montant_irg,
          payload.nif || null,
          payload.nin || null,
          payload.article_imposition || null,
          payload.code_activite || null,
          payload.date_versement,
          payload.delai_limite,
          payload.quittance_numero,
          payload.quittance_date,
          payload.lieu_declaration,
          payload.date_signature,
          payload.receveur_signature,
          payload.receveur_date,
          req.user?.sub || null,
        ],
      );

      const item = await fetchDeclarationById(client, companyId, rows[0].id);

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "fiscal.g50ter.create",
        entityType: "g50ter_declaration",
        entityId: item.id,
        newData: item,
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.status(201).json({
        message: "Declaration G50ter creee avec succes.",
        item,
      });
    } catch (error) {
      await client.query("ROLLBACK");

      if (error?.code === "23505") {
        throw new ApiError(
          409,
          "Une declaration G50ter existe deja pour ce trimestre et cette annee.",
        );
      }

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

  update: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    const declarationId = parseInteger(req.params.id, "Identifiant de declaration");
    const client = await pool.connect();

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

      const existingItem = await fetchDeclarationById(client, companyId, declarationId);

      if (existingItem.statut !== "brouillon") {
        throw new ApiError(409, "Seules les declarations brouillon peuvent etre modifiees.");
      }

      const [company, profile, fiscalYear] = await Promise.all([
        fetchCompany(client, companyId),
        fetchFiscalProfile(client, companyId),
        resolveFiscalYearContext(client, companyId, {
          fiscal_year_id: req.body.fiscal_year_id || existingItem.fiscal_year_id,
          annee: req.body.annee || existingItem.annee,
        }),
      ]);

      await ensureG50terDeclarations(client, {
        companyId,
        fiscalYear,
        userId: req.user?.sub || null,
      });

      const payload = buildDeclarationPayload({
        body: req.body,
        companyId,
        company,
        profile,
        fiscalYear,
        user: req.user,
        existingItem,
      });

      await client.query(
        `
          UPDATE g50ter_declarations
          SET
            fiscal_year_id = $2,
            trimestre = $3,
            annee = $4,
            salaires_imposables_t = $5,
            taux_bareme = $6,
            montant_irg = $7,
            nif = $8,
            nin = $9,
            article_imposition = $10,
            code_activite = $11,
            date_versement = $12,
            delai_limite = $13,
            quittance_numero = $14,
            quittance_date = $15,
            lieu_declaration = $16,
            date_signature = $17,
            receveur_signature = $18,
            receveur_date = $19,
            updated_at = CURRENT_TIMESTAMP
          WHERE id = $1
        `,
        [
          declarationId,
          payload.fiscal_year_id,
          payload.trimestre,
          payload.annee,
          payload.salaires_imposables_t,
          payload.taux_bareme,
          payload.montant_irg,
          payload.nif || null,
          payload.nin || null,
          payload.article_imposition || null,
          payload.code_activite || null,
          payload.date_versement,
          payload.delai_limite,
          payload.quittance_numero,
          payload.quittance_date,
          payload.lieu_declaration,
          payload.date_signature,
          payload.receveur_signature,
          payload.receveur_date,
        ],
      );

      const item = await fetchDeclarationById(client, companyId, declarationId);
      await markFiscalDeadlineAsFiled(client, {
        companyId,
        declarationType: "G50TER",
        fiscalYearId: item.fiscal_year_id,
        periodeLabel: `T${item.trimestre} ${item.annee}`,
        declarationId: item.id,
      });

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "fiscal.g50ter.update",
        entityType: "g50ter_declaration",
        entityId: item.id,
        oldData: existingItem,
        newData: item,
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.json({
        message: "Declaration G50ter mise a jour.",
        item,
      });
    } catch (error) {
      await client.query("ROLLBACK");

      if (error?.code === "23505") {
        throw new ApiError(
          409,
          "Une declaration G50ter existe deja pour ce trimestre et cette annee.",
        );
      }

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

  submit: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    const declarationId = parseInteger(req.params.id, "Identifiant de declaration");
    const client = await pool.connect();

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

      const existingItem = await fetchDeclarationById(client, companyId, declarationId);

      if (existingItem.statut !== "brouillon") {
        throw new ApiError(409, "Cette declaration G50ter est deja deposee.");
      }

      const dateVersement =
        parseOptionalIsoDate(
          req.body.date_versement || req.body.quittance_date || existingItem.quittance_date,
          "La date de versement",
        ) || null;
      const quittanceNumero =
        toNullableText(req.body.quittance_numero) || existingItem.quittance_numero || null;

      await client.query(
        `
          UPDATE g50ter_declarations
          SET
            statut = 'deposee',
            date_versement = $2,
            quittance_numero = $3,
            quittance_date = COALESCE($4, quittance_date),
            updated_at = CURRENT_TIMESTAMP
          WHERE id = $1
        `,
        [
          declarationId,
          dateVersement,
          quittanceNumero,
          parseOptionalIsoDate(
            req.body.quittance_date || existingItem.quittance_date,
            "La date de quittance",
          ),
        ],
      );

      const item = await fetchDeclarationById(client, companyId, declarationId);

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "fiscal.g50ter.submit",
        entityType: "g50ter_declaration",
        entityId: item.id,
        oldData: existingItem,
        newData: item,
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.json({
        message: "Declaration G50ter marquee comme deposee.",
        item,
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

  pdf: async (req, res) => {
    const companyId = getCompanyId(req);
    const declarationId = parseInteger(req.params.id, "Identifiant de declaration");
    const client = await pool.connect();

    try {
      const item = await fetchDeclarationById(client, companyId, declarationId);
      const [company, profile, yearItems] = await Promise.all([
        fetchCompany(client, companyId),
        fetchFiscalProfile(client, companyId),
        fetchYearDeclarations(client, companyId, item.annee),
      ]);

      const buffer = await buildG50terPdfBuffer({
        company,
        profile,
        declaration: item,
        yearItems,
        generatedBy: req.user?.fullName || req.user?.email || "Systeme",
      });

      res.setHeader("Content-Type", "application/pdf");
      res.setHeader(
        "Content-Disposition",
        `inline; filename="g50ter-${item.annee}-T${item.trimestre}.pdf"`,
      );
      res.send(Buffer.from(buffer));
    } finally {
      client.release();
    }
  },

  irgBaremeCalculator: async (req, res) => {
    try {
      const result = calculateIrgBareme({
        salaireBrut: req.body.salaire_brut,
        nbMois: req.body.nb_mois,
      });

      res.json({
        item: result,
      });
    } catch (error) {
      throw new ApiError(400, error.message || "Le calcul IRG a echoue.");
    }
  },
};
