import { pool } from "../db/pool.js";
import { ApiError } from "../utils/ApiError.js";
import { fetchRolePermissions } from "../utils/permissions.js";
import {
  calculateG50,
  getG50CalculationStatus,
  runG50Preflight,
} from "../services/g50CalculationEngine.js";
import {
  adjustG50DraftLine,
  buildG50DraftPdf,
  getG50Draft,
  getG50DraftControlChecks,
  getG50DraftLineEntries,
  listG50Corrections,
  listG50History,
  recalculateG50Draft,
  reopenG50Draft,
  revertG50DraftAdjustment,
  validateG50Draft,
} from "../services/g50DraftWorkflow.js";

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

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 ensureCompanyAccess = (req, companyId) => {
  if (req.user?.role === "super_admin") {
    return;
  }

  const allowedCompanyIds = new Set(
    [req.user?.companyId, ...(Array.isArray(req.user?.companyIds) ? req.user.companyIds : [])]
      .map((value) => Number(value || 0))
      .filter(Boolean),
  );

  if (!allowedCompanyIds.has(companyId)) {
    throw new ApiError(403, "Accès refusé à cette société.");
  }
};

const parsePeriod = (source) => {
  const periodFrom = cleanString(source.period_from || source.periodFrom);
  const periodTo = cleanString(source.period_to || source.periodTo);

  if (!periodFrom || !periodTo) {
    throw new ApiError(400, "period_from et period_to sont requis.");
  }

  return {
    periodFrom,
    periodTo,
  };
};

const getRoleContext = async (db, req) => ({
  role: req.user?.role || "",
  permissions:
    Array.isArray(req.user?.permissions) && req.user.permissions.length
      ? req.user.permissions
      : await fetchRolePermissions(db, req.user?.role || ""),
});

const getGeneratedByLabel = (req) =>
  cleanString(req.user?.fullName || req.user?.email || req.user?.role || "Systeme");

const withTransaction = async (handler) => {
  const client = await pool.connect();

  try {
    await client.query("BEGIN");
    const result = await handler(client);
    await client.query("COMMIT");
    return result;
  } catch (error) {
    await client.query("ROLLBACK");
    throw error;
  } finally {
    client.release();
  }
};

export const fiscalG50EngineController = {
  preflight: async (req, res) => {
    const companyId = getCompanyId(req, req.query);
    ensureCompanyAccess(req, companyId);

    const { periodFrom, periodTo } = parsePeriod(req.query);
    const item = await runG50Preflight({
      db: pool,
      companyId,
      periodFrom,
      periodTo,
    });

    res.json({ item });
  },

  calculate: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    ensureCompanyAccess(req, companyId);

    const { periodFrom, periodTo } = parsePeriod(req.body);
    const client = await pool.connect();

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

      const result = await calculateG50({
        db: client,
        companyId,
        periodFrom,
        periodTo,
        userId: req.user?.sub || null,
        io: req.app.get("io"),
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.status(201).json({
        message: "Brouillon G50 généré.",
        item: result,
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

  status: async (req, res) => {
    const companyId = getCompanyId(req, req.query);
    ensureCompanyAccess(req, companyId);

    const headerId = Number(req.params.g50_header_id || 0);

    if (!headerId) {
      throw new ApiError(400, "Identifiant de brouillon G50 invalide.");
    }

    const item = await getG50CalculationStatus({
      db: pool,
      companyId,
      headerId,
    });

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

    res.json({ item });
  },

  getDraft: async (req, res) => {
    const companyId = getCompanyId(req, req.query);
    ensureCompanyAccess(req, companyId);

    const headerId = Number(req.params.id || 0);

    if (!headerId) {
      throw new ApiError(400, "Identifiant de brouillon G50 invalide.");
    }

    const roleContext = await getRoleContext(pool, req);
    const item = await getG50Draft({
      db: pool,
      companyId,
      headerId,
      ...roleContext,
    });

    res.json({ item });
  },

  getLineEntries: async (req, res) => {
    const companyId = getCompanyId(req, req.query);
    ensureCompanyAccess(req, companyId);

    const headerId = Number(req.params.id || 0);
    const lineId = Number(req.params.line_id || 0);

    if (!headerId || !lineId) {
      throw new ApiError(400, "Paramètres de ligne G50 invalides.");
    }

    const item = await getG50DraftLineEntries({
      db: pool,
      companyId,
      headerId,
      lineId,
    });

    res.json({ item });
  },

  adjustLine: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    ensureCompanyAccess(req, companyId);

    const headerId = Number(req.params.id || 0);
    const lineId = Number(req.params.line_id || 0);

    if (!headerId || !lineId) {
      throw new ApiError(400, "Paramètres de ligne G50 invalides.");
    }

    const item = await withTransaction(async (client) => {
      const roleContext = await getRoleContext(client, req);

      return adjustG50DraftLine({
        db: client,
        companyId,
        headerId,
        lineId,
        newAmount: req.body.new_amount,
        reasonType: req.body.reason_type,
        justification: req.body.justification,
        supportingDocumentUrl: req.body.supporting_document_url,
        userId: req.user?.sub || null,
        ipAddress: req.ip,
        ...roleContext,
      });
    });

    res.json({
      message: "Ajustement G50 enregistré.",
      item,
    });
  },

  revertAdjustment: async (req, res) => {
    const companyId = getCompanyId(req, req.query);
    ensureCompanyAccess(req, companyId);

    const headerId = Number(req.params.id || 0);
    const lineId = Number(req.params.line_id || 0);

    if (!headerId || !lineId) {
      throw new ApiError(400, "Paramètres de ligne G50 invalides.");
    }

    const item = await withTransaction(async (client) => {
      const roleContext = await getRoleContext(client, req);

      return revertG50DraftAdjustment({
        db: client,
        companyId,
        headerId,
        lineId,
        userId: req.user?.sub || null,
        ipAddress: req.ip,
        ...roleContext,
      });
    });

    res.json({
      message: "Ajustement G50 annulé.",
      item,
    });
  },

  validateDraft: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    ensureCompanyAccess(req, companyId);

    const headerId = Number(req.params.id || 0);

    if (!headerId) {
      throw new ApiError(400, "Identifiant de brouillon G50 invalide.");
    }

    const item = await withTransaction(async (client) => {
      const roleContext = await getRoleContext(client, req);

      return validateG50Draft({
        db: client,
        companyId,
        headerId,
        userId: req.user?.sub || null,
        confirmationText: req.body.confirmation_text,
        io: req.app.get("io"),
        ipAddress: req.ip,
        ...roleContext,
      });
    });

    res.json({
      message: "G50 validée et verrouillée.",
      item,
    });
  },

  reopenDraft: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    ensureCompanyAccess(req, companyId);

    const headerId = Number(req.params.id || 0);

    if (!headerId) {
      throw new ApiError(400, "Identifiant de brouillon G50 invalide.");
    }

    const item = await withTransaction(async (client) => {
      const roleContext = await getRoleContext(client, req);

      return reopenG50Draft({
        db: client,
        companyId,
        headerId,
        userId: req.user?.sub || null,
        justification: req.body.justification,
        ipAddress: req.ip,
        ...roleContext,
      });
    });

    res.json({
      message: "G50 rouverte.",
      item,
    });
  },

  recalculateDraft: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    ensureCompanyAccess(req, companyId);

    const headerId = Number(req.params.id || 0);

    if (!headerId) {
      throw new ApiError(400, "Identifiant de brouillon G50 invalide.");
    }

    const item = await withTransaction(async (client) => {
      const roleContext = await getRoleContext(client, req);

      return recalculateG50Draft({
        db: client,
        companyId,
        headerId,
        userId: req.user?.sub || null,
        io: req.app.get("io"),
        ipAddress: req.ip,
        ...roleContext,
      });
    });

    res.json({
      message: "Brouillon G50 recalculé.",
      item,
    });
  },

  getControlChecks: async (req, res) => {
    const companyId = getCompanyId(req, req.query);
    ensureCompanyAccess(req, companyId);

    const headerId = Number(req.params.id || 0);

    if (!headerId) {
      throw new ApiError(400, "Identifiant de brouillon G50 invalide.");
    }

    const item = await getG50DraftControlChecks({
      db: pool,
      companyId,
      headerId,
    });

    res.json({ item });
  },

  listCorrections: async (req, res) => {
    const companyId = getCompanyId(req, req.query);
    ensureCompanyAccess(req, companyId);

    const item = await listG50Corrections({
      db: pool,
      companyId,
      periodFrom: cleanString(req.query.period_from || req.query.periodFrom),
      periodTo: cleanString(req.query.period_to || req.query.periodTo),
    });

    res.json(item);
  },

  listHistory: async (req, res) => {
    const companyId = getCompanyId(req, req.query);
    ensureCompanyAccess(req, companyId);

    const roleContext = await getRoleContext(pool, req);
    const item = await listG50History({
      db: pool,
      companyId,
      annee: req.query.annee || null,
      ...roleContext,
    });

    res.json(item);
  },

  exportDraftPdf: async (req, res) => {
    const companyId = getCompanyId(req, req.query);
    ensureCompanyAccess(req, companyId);

    const headerId = Number(req.params.id || 0);

    if (!headerId) {
      throw new ApiError(400, "Identifiant de brouillon G50 invalide.");
    }

    const pdfBuffer = await buildG50DraftPdf({
      db: pool,
      companyId,
      headerId,
      generatedBy: getGeneratedByLabel(req),
    });

    res.setHeader("Content-Type", "application/pdf");
    res.setHeader(
      "Content-Disposition",
      `inline; filename=\"g50-brouillon-${headerId}.pdf\"`,
    );
    res.send(Buffer.from(pdfBuffer));
  },
};
