import { FastifyInstance } from "fastify";
import prisma from "../../config/database";
import { requireAnyPermission, requirePermission } from "../../middleware/auth";
import { logAudit } from "../../services/audit";
import { success, paginated, error, parseId } from "../../utils/response";
import { parsePagination, buildPaginationMeta } from "../../utils/pagination";
import { parseBody } from "../../schemas/common";
import { CreateTripSchema, UpdateTripSchema } from "../../schemas/trips.schema";
import { AuthData } from "../../types";

/**
 * Shared filter/scoping logic for the trip list AND the stats endpoint — the
 * stats MUST honor exactly the same filters as the list so the cards always
 * describe the rows the list is paging through.
 *
 * Scoping: admins / trips.manage holders see all trips (optionally filtered
 * by user_id); everyone else is hard-scoped to their own trips.
 */
function buildTripsWhere(
  query: Record<string, unknown>,
  authData: AuthData,
): Record<string, unknown> {
  // Admin role bypasses permission checks entirely (requireAnyPermission
  // lets it through with an empty permissions list), so treat the admin
  // role as a manager for scoping purposes too.
  const isManager =
    authData.roleName === "admin" ||
    authData.permissions.includes("trips.manage");

  const where: Record<string, unknown> = {};
  if (!isManager) where.user_id = authData.userId;
  else if (query.user_id) {
    // Non-numeric filter values are ignored (like an invalid month below) —
    // NaN in a Prisma where clause would throw and surface as a 500.
    const uid = Number(query.user_id);
    if (Number.isFinite(uid)) where.user_id = uid;
  }
  if (query.vehicle_id) {
    const vid = Number(query.vehicle_id);
    if (Number.isFinite(vid)) where.vehicle_id = vid;
  }

  // Support both "month=3&year=2026" (TripsAdmin) and "month=2026-03" (TripsHistory) formats
  if (query.month) {
    const monthStr = String(query.month);
    let yr: number, mo: number;
    if (monthStr.includes("-")) {
      // Combined YYYY-MM format
      const [yStr, mStr] = monthStr.split("-");
      yr = Number(yStr);
      mo = Number(mStr);
    } else if (query.year) {
      yr = Number(query.year);
      mo = Number(query.month);
    } else {
      yr = NaN;
      mo = NaN;
    }
    if (!isNaN(yr) && !isNaN(mo) && mo >= 1 && mo <= 12) {
      // Use explicit date strings to avoid toJSON timezone shift
      const monthStart = `${yr}-${String(mo).padStart(2, "0")}-01`;
      const nextMonth =
        mo === 12
          ? `${yr + 1}-01-01`
          : `${yr}-${String(mo + 1).padStart(2, "0")}-01`;
      where.trip_date = {
        gte: new Date(monthStart),
        lt: new Date(nextMonth),
      };
    }
  }

  return where;
}

/**
 * Recompute a vehicle's actual_km as MAX(end_km) over its trips, falling
 * back to initial_km when the vehicle has no trips. Shared by the POST,
 * PUT and DELETE handlers so the odometer logic can't drift.
 */
async function recomputeVehicleActualKm(vehicleId: number): Promise<void> {
  const [maxTrip, vehicle] = await Promise.all([
    prisma.trips.findFirst({
      where: { vehicle_id: vehicleId },
      orderBy: { end_km: "desc" },
      select: { end_km: true },
    }),
    prisma.vehicles.findUnique({
      where: { id: vehicleId },
      select: { initial_km: true },
    }),
  ]);
  await prisma.vehicles.update({
    where: { id: vehicleId },
    data: {
      actual_km: maxTrip
        ? Number(maxTrip.end_km)
        : Number(vehicle?.initial_km ?? 0),
    },
  });
}

export default async function tripsRoutes(
  fastify: FastifyInstance,
): Promise<void> {
  fastify.get(
    "/",
    {
      preHandler: requireAnyPermission(
        "trips.manage",
        "trips.record",
        "trips.history",
      ),
    },
    async (request, reply) => {
      const query = request.query as Record<string, unknown>;
      const { page, limit, skip, order } = parsePagination(query);
      const where = buildTripsWhere(query, request.authData!);

      const [trips, total] = await Promise.all([
        prisma.trips.findMany({
          where,
          skip,
          take: limit,
          // trip_date is date-only — same-day rows need the id tiebreak for
          // deterministic pagination.
          orderBy: [{ trip_date: order }, { id: order }],
          include: {
            users: { select: { id: true, first_name: true, last_name: true } },
            vehicles: { select: { id: true, name: true, spz: true } },
          },
        }),
        prisma.trips.count({ where }),
      ]);

      return paginated(reply, trips, buildPaginationMeta(total, page, limit));
    },
  );

  // GET /api/admin/trips/stats — count/km totals over the WHOLE filtered set
  // (not one page). Honors exactly the same filters + user scoping as the
  // list (month/year, vehicle_id, user_id for managers) so the stat cards
  // can't drift from the rows the list is paging through.
  fastify.get(
    "/stats",
    {
      preHandler: requireAnyPermission(
        "trips.manage",
        "trips.record",
        "trips.history",
      ),
    },
    async (request, reply) => {
      const query = request.query as Record<string, unknown>;
      const where = buildTripsWhere(query, request.authData!);

      // Legacy PHP-era rows can carry a stored `distance` that differs from
      // end_km - start_km, and every row renderer prefers it
      // (`distance ?? end_km - start_km`) — the totals must apply the same
      // coalesce or the stat cards would disagree with the visible rows.
      // That rules out a plain groupBy aggregate.
      const rows = await prisma.trips.findMany({
        where,
        select: {
          distance: true,
          start_km: true,
          end_km: true,
          is_business: true,
        },
      });

      let count = 0;
      let businessKm = 0;
      let privateKm = 0;
      for (const t of rows) {
        const km =
          t.distance != null
            ? Number(t.distance)
            : Number(t.end_km) - Number(t.start_km);
        count += 1;
        if (t.is_business) businessKm += km;
        else privateKm += km;
      }

      return success(reply, {
        count,
        total_km: businessKm + privateKm,
        business_km: businessKm,
        private_km: privateKm,
      });
    },
  );

  // GET /api/admin/trips/users — users with trips.record permission
  fastify.get(
    "/users",
    { preHandler: requireAnyPermission("trips.record", "trips.manage") },
    async (_request, reply) => {
      const users = await prisma.users.findMany({
        where: {
          is_active: true,
          roles: {
            role_permissions: {
              some: { permissions: { name: "trips.record" } },
            },
          },
        },
        select: {
          id: true,
          first_name: true,
          last_name: true,
          username: true,
        },
        orderBy: { last_name: "asc" },
      });
      return success(
        reply,
        users.map((u) => ({
          id: u.id,
          name: `${u.first_name} ${u.last_name}`.trim() || u.username,
        })),
      );
    },
  );

  // GET /api/admin/trips/print — print data for trip report
  fastify.get(
    "/print",
    { preHandler: requirePermission("trips.manage") },
    async (request, reply) => {
      const query = request.query as Record<string, unknown>;
      // Same shared filter source as the list + stats endpoints (incl. the
      // month 1-12 validation). /print is manager-only (trips.manage guard),
      // so buildTripsWhere always takes its manager branch here — user_id /
      // vehicle_id / month filters behave exactly as before.
      const where = buildTripsWhere(query, request.authData!);

      const trips = await prisma.trips.findMany({
        where,
        include: {
          users: { select: { id: true, first_name: true, last_name: true } },
          vehicles: { select: { id: true, name: true, spz: true } },
        },
        // trip_date is date-only — id tiebreak keeps same-day rows stable
        orderBy: [{ trip_date: "asc" }, { id: "asc" }],
      });

      const vehicles = await prisma.vehicles.findMany({
        orderBy: { name: "asc" },
      });
      const users = await prisma.users.findMany({
        where: { is_active: true },
        select: { id: true, first_name: true, last_name: true },
        orderBy: { last_name: "asc" },
      });

      let totalKm = 0;
      let businessKm = 0;
      let privateKm = 0;
      for (const t of trips) {
        // Same coalesce as the row renderers and /stats: legacy rows may
        // store a `distance` differing from end_km - start_km, and the print
        // footer must match the sum of the printed rows.
        const dist =
          t.distance != null
            ? Number(t.distance)
            : Number(t.end_km) - Number(t.start_km);
        totalKm += dist;
        if (t.is_business) businessKm += dist;
        else privateKm += dist;
      }

      return success(reply, {
        trips,
        vehicles,
        users: users.map((u) => ({
          id: u.id,
          name: `${u.first_name} ${u.last_name}`.trim(),
        })),
        totals: {
          total_km: totalKm,
          business_km: businessKm,
          private_km: privateKm,
          count: trips.length,
        },
      });
    },
  );

  // GET /api/admin/trips/last-km/:vehicleId
  // Matches PHP: COALESCE(MAX(end_km), vehicle.initial_km, 0)
  fastify.get<{ Params: { vehicleId: string } }>(
    "/last-km/:vehicleId",
    { preHandler: requirePermission("trips.manage") },
    async (request, reply) => {
      const vehicleId = parseId((request.params as any).vehicleId, reply);
      if (vehicleId === null) return;

      const [lastTrip, vehicle] = await Promise.all([
        prisma.trips.findFirst({
          where: { vehicle_id: vehicleId },
          orderBy: { end_km: "desc" },
          select: { end_km: true },
        }),
        prisma.vehicles.findUnique({
          where: { id: vehicleId },
          select: { initial_km: true },
        }),
      ]);

      const lastKm = lastTrip
        ? Number(lastTrip.end_km)
        : Number(vehicle?.initial_km ?? 0);

      return success(reply, { last_km: lastKm });
    },
  );

  fastify.post(
    "/",
    { preHandler: requireAnyPermission("trips.manage", "trips.record") },
    async (request, reply) => {
      const parsed = parseBody(CreateTripSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;
      const authData = request.authData!;

      if (body.end_km < body.start_km) {
        return error(
          reply,
          "Konečný stav km nesmí být menší než počáteční",
          400,
        );
      }

      const vehicleId = Number(body.vehicle_id);

      const trip = await prisma.trips.create({
        data: {
          vehicle_id: vehicleId,
          user_id: body.user_id ? Number(body.user_id) : authData.userId,
          trip_date: new Date(String(body.trip_date)),
          start_km: Number(body.start_km),
          end_km: Number(body.end_km),
          route_from: String(body.route_from),
          route_to: String(body.route_to),
          is_business: !!body.is_business,
          notes: body.notes ? String(body.notes) : null,
        },
      });

      // Recompute actual_km from MAX(end_km) across all trips (same helper
      // as PUT/DELETE). Setting it unconditionally to this trip's end_km
      // would regress the odometer when a back-dated trip with a lower
      // end_km is entered after a later trip with a higher reading. (The
      // helper's initial_km fallback is unreachable here — the just-created
      // trip guarantees at least one row.)
      await recomputeVehicleActualKm(vehicleId);

      await logAudit({
        request,
        authData,
        action: "create",
        entityType: "trip",
        entityId: trip.id,
        description: `Vytvořena jízda`,
      });
      return success(reply, { id: trip.id }, 201, "Jízda byla zaznamenána");
    },
  );

  fastify.put<{ Params: { id: string } }>(
    "/:id",
    { preHandler: requireAnyPermission("trips.manage", "trips.record") },
    async (request, reply) => {
      const id = parseId((request.params as any).id, reply);
      if (id === null) return;
      const parsed = parseBody(UpdateTripSchema, request.body);
      if ("error" in parsed) return error(reply, parsed.error, 400);
      const body = parsed.data;
      const authData = request.authData!;

      if (
        body.end_km != null &&
        body.start_km != null &&
        body.end_km < body.start_km
      ) {
        return error(
          reply,
          "Konečný stav km nesmí být menší než počáteční",
          400,
        );
      }

      const existing = await prisma.trips.findUnique({ where: { id } });
      if (!existing) return error(reply, "Jízda nenalezena", 404);

      // Ownership check — same as DELETE handler
      const isAdmin = authData.permissions.includes("trips.manage");
      if (existing.user_id !== authData.userId && !isAdmin) {
        return error(reply, "Nemáte oprávnění upravit tuto jízdu", 403);
      }

      const data: Record<string, unknown> = {};
      if (body.vehicle_id !== undefined) {
        const targetVehicleId = Number(body.vehicle_id);
        if (targetVehicleId !== existing.vehicle_id) {
          // Schema only guarantees a positive integer — without this check a
          // nonexistent id would hit the FK as P2003 and surface as a 500.
          const vehicleExists = await prisma.vehicles.findUnique({
            where: { id: targetVehicleId },
            select: { id: true },
          });
          if (!vehicleExists) return error(reply, "Vozidlo nenalezeno", 400);
        }
        data.vehicle_id = targetVehicleId;
      }
      if (body.trip_date !== undefined)
        data.trip_date = new Date(String(body.trip_date));
      if (body.start_km !== undefined) data.start_km = Number(body.start_km);
      if (body.end_km !== undefined) data.end_km = Number(body.end_km);
      if (body.route_from !== undefined)
        data.route_from = String(body.route_from);
      if (body.route_to !== undefined) data.route_to = String(body.route_to);
      if (body.is_business !== undefined) data.is_business = !!body.is_business;
      if (body.notes !== undefined)
        data.notes = body.notes ? String(body.notes) : null;

      await prisma.trips.update({ where: { id }, data });

      // Recompute odometers when the km reading or the vehicle changed: the
      // trip's (possibly new) vehicle, and — when the trip moved between
      // vehicles — the old one too, which may have lost its MAX(end_km) row.
      const newVehicleId =
        body.vehicle_id !== undefined
          ? Number(body.vehicle_id)
          : existing.vehicle_id;
      const vehicleChanged = newVehicleId !== existing.vehicle_id;
      if (body.end_km !== undefined || vehicleChanged) {
        await recomputeVehicleActualKm(newVehicleId);
      }
      if (vehicleChanged) {
        await recomputeVehicleActualKm(existing.vehicle_id);
      }

      await logAudit({
        request,
        authData,
        action: "update",
        entityType: "trip",
        entityId: id,
        description: `Upravena jízda`,
        oldValues: existing,
        newValues: data,
      });
      return success(reply, { id }, 200, "Záznam byl aktualizován");
    },
  );

  fastify.delete<{ Params: { id: string } }>(
    "/:id",
    { preHandler: requireAnyPermission("trips.manage", "trips.record") },
    async (request, reply) => {
      const id = parseId((request.params as any).id, reply);
      if (id === null) return;
      const authData = request.authData!;
      const existing = await prisma.trips.findUnique({ where: { id } });
      if (!existing) return error(reply, "Jízda nenalezena", 404);

      // Allow users to delete their own trips, admins can delete any
      const isAdmin = authData.permissions.includes("trips.manage");
      if (existing.user_id !== authData.userId && !isAdmin) {
        return error(reply, "Nemáte oprávnění smazat tuto jízdu", 403);
      }

      const vehicleId = existing.vehicle_id;
      await prisma.trips.delete({ where: { id } });

      // Recalculate vehicle actual_km after deletion (falls back to
      // initial_km when this was the vehicle's last trip).
      await recomputeVehicleActualKm(vehicleId);

      await logAudit({
        request,
        authData,
        action: "delete",
        entityType: "trip",
        entityId: id,
        description: `Smazána jízda`,
      });
      return success(reply, { id }, 200, "Záznam byl smazán");
    },
  );
}
