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

const ADMIN_ROLES = new Set(["super_admin", "manager"]);
const PERIOD_MANAGER_ROLES = new Set(["super_admin", "manager", "chief_accountant"]);

const MONTH_LABELS_FR = [
  "Janvier",
  "Février",
  "Mars",
  "Avril",
  "Mai",
  "Juin",
  "Juillet",
  "Août",
  "Septembre",
  "Octobre",
  "Novembre",
  "Décembre",
];

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 ensureRole = (role, allowedRoles, message) => {
  if (!allowedRoles.has(role)) {
    throw new ApiError(403, message);
  }
};

const fetchFiscalYearContext = async (db, companyId, fiscalYearId = null) => {
  const { rows } = await db.query(
    `
      SELECT
        id,
        company_id,
        label,
        TO_CHAR(start_date, 'YYYY-MM-DD') AS start_date,
        TO_CHAR(end_date, 'YYYY-MM-DD') AS end_date,
        is_closed,
        is_current
      FROM fiscal_years
      WHERE company_id = $1
        AND ($2::bigint IS NULL OR id = $2)
      ORDER BY
        CASE WHEN $2::bigint IS NULL THEN is_current::int ELSE 1 END DESC,
        start_date DESC
      LIMIT 1
    `,
    [companyId, fiscalYearId],
  );

  return rows[0] || null;
};

const fetchPeriodById = async (db, companyId, periodId) => {
  const { rows } = await db.query(
    `
      SELECT ap.*, fy.label AS fiscal_year_label
      FROM accounting_periods ap
      JOIN fiscal_years fy
        ON fy.id = ap.fiscal_year_id
      WHERE ap.id = $1
        AND ap.company_id = $2
      LIMIT 1
    `,
    [periodId, companyId],
  );

  return rows[0] || null;
};

const fetchDraftEntries = async (db, periodId) => {
  const { rows } = await db.query(
    `
      SELECT
        je.id,
        je.entry_number,
        je.entry_date,
        je.label,
        j.code AS journal_code
      FROM journal_entries je
      JOIN journals j
        ON j.id = je.journal_id
      WHERE je.period_id = $1
        AND je.status = 'draft'
      ORDER BY je.entry_date DESC, je.id DESC
    `,
    [periodId],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    entry_number: row.entry_number,
    entry_date: formatDateOnly(row.entry_date),
    label: row.label,
    journal_code: row.journal_code,
  }));
};

const refreshFiscalYearClosedStatus = async (db, fiscalYearId) => {
  await db.query(
    `
      UPDATE fiscal_years fy
      SET is_closed = NOT EXISTS (
        SELECT 1
        FROM accounting_periods ap
        WHERE ap.fiscal_year_id = fy.id
          AND ap.is_closed = FALSE
      )
      WHERE fy.id = $1
    `,
    [fiscalYearId],
  );
};

export const periodsController = {
  list: async (req, res) => {
    const companyId = getCompanyId(req);
    const requestedFiscalYearId = req.query.fiscal_year_id
      ? Number(req.query.fiscal_year_id)
      : null;
    const client = await pool.connect();

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

      const fiscalYear = await fetchFiscalYearContext(
        client,
        companyId,
        requestedFiscalYearId,
      );

      if (!fiscalYear) {
        throw new ApiError(404, "Aucun exercice comptable disponible.");
      }

      const syncedPeriods = await syncFiscalYearPeriods(client, fiscalYear);

      if (syncedPeriods.length) {
        await logAudit(client, {
          companyId,
          userId: req.user?.sub || null,
          action: "periods.sync",
          entityType: "fiscal_year",
          entityId: fiscalYear.id,
          newData: {
            generated_periods: syncedPeriods.length,
          },
          ipAddress: req.ip,
        });
      }

      const { rows } = await client.query(
        `
          SELECT
            ap.id,
            ap.month,
            ap.year,
            ap.is_closed,
            ap.closed_at,
            ap.closed_by,
            COALESCE(entry_stats.entries_count, 0)::int AS entries_count,
            COALESCE(entry_stats.draft_entries_count, 0)::int AS draft_entries_count,
            COALESCE(draft_entries.entries, '[]'::json) AS draft_entries
          FROM accounting_periods ap
          LEFT JOIN LATERAL (
            SELECT
              COUNT(*) FILTER (WHERE je.status <> 'cancelled') AS entries_count,
              COUNT(*) FILTER (WHERE je.status = 'draft') AS draft_entries_count
            FROM journal_entries je
            WHERE je.period_id = ap.id
          ) AS entry_stats
            ON TRUE
          LEFT JOIN LATERAL (
            SELECT COALESCE(
              json_agg(
                json_build_object(
                  'id', draft_source.id,
                  'entry_number', draft_source.entry_number,
                  'entry_date', draft_source.entry_date,
                  'label', draft_source.label,
                  'journal_code', draft_source.journal_code
                )
                ORDER BY draft_source.entry_date DESC
              ),
              '[]'::json
            ) AS entries
            FROM (
              SELECT
                je.id,
                je.entry_number,
                je.entry_date,
                je.label,
                j.code AS journal_code
              FROM journal_entries je
              JOIN journals j
                ON j.id = je.journal_id
              WHERE je.period_id = ap.id
                AND je.status = 'draft'
              ORDER BY je.entry_date DESC, je.id DESC
              LIMIT 10
            ) AS draft_source
          ) AS draft_entries
            ON TRUE
          WHERE ap.company_id = $1
            AND ap.fiscal_year_id = $2
          ORDER BY ap.year ASC, ap.month ASC
        `,
        [companyId, fiscalYear.id],
      );

      await client.query("COMMIT");

      res.json({
        company_id: companyId,
        fiscal_year: {
          id: Number(fiscalYear.id),
          label: fiscalYear.label,
          start_date: formatDateOnly(fiscalYear.start_date),
          end_date: formatDateOnly(fiscalYear.end_date),
          is_closed: fiscalYear.is_closed,
          is_current: fiscalYear.is_current,
        },
        permissions: {
          can_close: PERIOD_MANAGER_ROLES.has(req.user?.role),
          can_reopen: ADMIN_ROLES.has(req.user?.role),
        },
        items: rows.map((row) => ({
          id: Number(row.id),
          month: Number(row.month),
          year: Number(row.year),
          month_label: MONTH_LABELS_FR[Number(row.month) - 1],
          is_closed: row.is_closed,
          status_label: row.is_closed ? "Clôturé" : "Ouvert",
          entries_count: Number(row.entries_count),
          draft_entries_count: Number(row.draft_entries_count),
          draft_entries: row.draft_entries,
          closed_at: row.closed_at ? row.closed_at.toISOString() : null,
          closed_by: row.closed_by ? Number(row.closed_by) : null,
        })),
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

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

    if (!periodId) {
      throw new ApiError(400, "Identifiant de période invalide.");
    }

    ensureRole(
      req.user?.role,
      PERIOD_MANAGER_ROLES,
      "Vous n'êtes pas autorisé à clôturer une période.",
    );

    const client = await pool.connect();

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

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

      if (!period) {
        throw new ApiError(404, "Période comptable introuvable.");
      }

      const draftEntries = await fetchDraftEntries(client, periodId);

      if (draftEntries.length && req.body.confirm_with_drafts !== true) {
        throw new ApiError(
          409,
          "Des écritures brouillon existent encore sur cette période.",
        );
      }

      const { rows } = await client.query(
        `
          UPDATE accounting_periods
          SET
            is_closed = TRUE,
            closed_at = CURRENT_TIMESTAMP,
            closed_by = $2
          WHERE id = $1
            AND company_id = $3
          RETURNING *
        `,
        [periodId, req.user?.sub || null, companyId],
      );

      await refreshFiscalYearClosedStatus(client, period.fiscal_year_id);

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "periods.close",
        entityType: "accounting_period",
        entityId: periodId,
        oldData: {
          id: period.id,
          is_closed: period.is_closed,
          closed_at: period.closed_at,
          closed_by: period.closed_by,
        },
        newData: {
          id: rows[0].id,
          is_closed: rows[0].is_closed,
          closed_at: rows[0].closed_at,
          closed_by: rows[0].closed_by,
          draft_entries_count: draftEntries.length,
        },
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      req.app
        .get("io")
        ?.to(`company:${companyId}`)
        .emit("period:closed", {
          periodId,
          label: `${MONTH_LABELS_FR[Number(rows[0].month) - 1]} ${rows[0].year}`,
        });

      const notifications = await createCompanyNotifications(pool, {
        companyId,
        type: "period_closed",
        message: `Période ${MONTH_LABELS_FR[Number(rows[0].month) - 1]} ${rows[0].year} clôturée`,
        entityId: periodId,
      });

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

      res.json({
        message: "Période clôturée avec succès.",
        item: {
          ...rows[0],
          draft_entries: draftEntries,
        },
      });
    } catch (error) {
      await client.query("ROLLBACK");

      if (error instanceof ApiError && error.statusCode === 409) {
        const draftEntries = await fetchDraftEntries(pool, periodId);

        return res.status(409).json({
          message: error.message,
          affected_entries: draftEntries,
        });
      }

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

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

    if (!periodId) {
      throw new ApiError(400, "Identifiant de période invalide.");
    }

    ensureRole(
      req.user?.role,
      ADMIN_ROLES,
      "La réouverture est réservée à l'administration.",
    );

    const client = await pool.connect();

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

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

      if (!period) {
        throw new ApiError(404, "Période comptable introuvable.");
      }

      const { rows } = await client.query(
        `
          UPDATE accounting_periods
          SET
            is_closed = FALSE,
            closed_at = NULL,
            closed_by = NULL
          WHERE id = $1
            AND company_id = $2
          RETURNING *
        `,
        [periodId, companyId],
      );

      await refreshFiscalYearClosedStatus(client, period.fiscal_year_id);

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "periods.reopen",
        entityType: "accounting_period",
        entityId: periodId,
        oldData: {
          id: period.id,
          is_closed: period.is_closed,
          closed_at: period.closed_at,
          closed_by: period.closed_by,
        },
        newData: {
          id: rows[0].id,
          is_closed: rows[0].is_closed,
          closed_at: rows[0].closed_at,
          closed_by: rows[0].closed_by,
        },
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

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