import { Router } from 'express';
import { LeaveRequestStatus, Prisma, SystemRole } from '@prisma/client';
import { z } from 'zod';
import { prisma } from '../db';
import { toDateOnly } from '../lib/dates';
import { badRequest, forbidden, notFound } from '../lib/errors';
import { param } from '../lib/params';
import { assertCan } from '../lib/rbac';
import { AuthedRequest, requireAuth } from '../middleware/auth';
import {
  serializeAllocation,
  serializeHoliday,
  serializeLeaveRequest,
  serializeLeaveType,
} from '../serializers/leave';
import {
  assertCanApply,
  computeBalance,
  leaveDaysBetween,
  normalizeDayPeriod,
  notifyEmployee,
  notifySuperAdmins,
} from '../services/leave';

export const leaveRouter = Router();
leaveRouter.use(requireAuth);

const empSelect = { id: true, name: true, code: true, avatarColor: true } as const;

const requestInclude = {
  employee: { select: empSelect },
  leaveType: true,
  decidedBy: { select: empSelect },
} satisfies Prisma.LeaveRequestInclude;

function requireSuperAdmin(req: AuthedRequest) {
  if (req.user!.role !== SystemRole.SuperAdmin) {
    throw forbidden('Super Admin only');
  }
}

function yearFromQuery(raw: unknown): number {
  const n = Number(raw);
  if (Number.isFinite(n) && n >= 2000 && n <= 2100) return Math.floor(n);
  return new Date().getFullYear();
}

leaveRouter.get('/types', async (_req, res, next) => {
  try {
    const rows = await prisma.leaveType.findMany({ orderBy: { code: 'asc' } });
    res.json({ types: rows.map(serializeLeaveType) });
  } catch (err) {
    next(err);
  }
});

leaveRouter.get('/balances', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    let employeeId = req.query.employeeId ? String(req.query.employeeId) : user.id;
    if (employeeId !== user.id && user.role !== SystemRole.SuperAdmin) {
      throw forbidden('Cannot view another employee\'s balances');
    }
    const year = yearFromQuery(req.query.year);
    const types = await prisma.leaveType.findMany({ orderBy: { code: 'asc' } });
    const balances = [];
    for (const t of types) {
      const bal = await computeBalance(employeeId, t.id, year);
      balances.push({
        ...bal,
        type: serializeLeaveType(t),
      });
    }
    const pendingCount = await prisma.leaveRequest.count({
      where: { employeeId, status: LeaveRequestStatus.to_approve },
    });
    res.json({ year, employeeId, balances, pendingCount });
  } catch (err) {
    next(err);
  }
});

leaveRouter.get('/requests', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    const status = req.query.status ? String(req.query.status) : '';
    const year = req.query.year != null && req.query.year !== ''
      ? yearFromQuery(req.query.year)
      : null;
    let employeeId = req.query.employeeId ? String(req.query.employeeId) : '';

    const where: Prisma.LeaveRequestWhereInput = {};

    if (user.role !== SystemRole.SuperAdmin) {
      where.employeeId = user.id;
    } else if (employeeId) {
      where.employeeId = employeeId;
    }

    if (status && status !== 'all') {
      if (!['to_approve', 'validated', 'refused'].includes(status)) {
        throw badRequest('Invalid status filter');
      }
      where.status = status as LeaveRequestStatus;
    }

    let rows = await prisma.leaveRequest.findMany({
      where,
      include: requestInclude,
      orderBy: [{ fromDate: 'desc' }, { createdAt: 'desc' }],
      take: 1000,
    });

    if (year != null) {
      rows = rows.filter((r) => r.fromDate.getUTCFullYear() === year);
    }

    res.json({ requests: rows.map(serializeLeaveRequest) });
  } catch (err) {
    next(err);
  }
});

const applySchema = z.object({
  typeId: z.string().min(1),
  fromDate: z.string().min(1),
  toDate: z.string().min(1),
  dayPeriod: z.enum(['full', 'morning', 'afternoon']).default('full'),
  description: z.string().optional(),
});

leaveRouter.post('/requests', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    assertCan(user, 'apply_leave');
    const body = applySchema.parse(req.body || {});
    const year = Number(body.fromDate.slice(0, 4)) || new Date().getFullYear();

    const { days, period, type } = await assertCanApply({
      employeeId: user.id,
      typeId: body.typeId,
      fromDate: body.fromDate,
      toDate: body.toDate,
      dayPeriod: body.dayPeriod,
      year,
    });

    const fromDate = toDateOnly(body.fromDate);
    const toDate = toDateOnly(body.toDate);
    if (!fromDate || !toDate) throw badRequest('Invalid dates');

    const row = await prisma.leaveRequest.create({
      data: {
        employeeId: user.id,
        typeId: body.typeId,
        fromDate,
        toDate,
        dayPeriod: period,
        days,
        description: body.description || '',
        status: LeaveRequestStatus.to_approve,
      },
      include: requestInclude,
    });

    await notifySuperAdmins(
      `${user.name} requested ${days} day(s) ${type.label}`
    );

    res.status(201).json({ request: serializeLeaveRequest(row) });
  } catch (err) {
    next(err);
  }
});

const decideSchema = z.object({
  status: z.enum(['validated', 'refused']),
  note: z.string().optional(),
});

leaveRouter.post('/requests/:id/decide', async (req: AuthedRequest, res, next) => {
  try {
    requireSuperAdmin(req);
    assertCan(req.user!, 'manage_leave');
    const id = param(req.params.id);
    const body = decideSchema.parse(req.body || {});

    const existing = await prisma.leaveRequest.findUnique({
      where: { id },
      include: { employee: true, leaveType: true },
    });
    if (!existing) throw notFound('Leave request not found');
    if (existing.status !== LeaveRequestStatus.to_approve) {
      throw badRequest('Request is already decided');
    }

    const row = await prisma.leaveRequest.update({
      where: { id },
      data: {
        status: body.status as LeaveRequestStatus,
        decidedById: req.user!.id,
        decidedAt: new Date(),
        decisionNote: body.note || '',
      },
      include: requestInclude,
    });

    const empName = existing.employee?.name || existing.employeeId;
    await notifyEmployee(
      existing.employeeId,
      `Leave request for ${empName} was ${
        body.status === 'validated' ? 'approved' : 'denied'
      }`
    );

    res.json({ request: serializeLeaveRequest(row) });
  } catch (err) {
    next(err);
  }
});

leaveRouter.get('/allocations', async (req: AuthedRequest, res, next) => {
  try {
    requireSuperAdmin(req);
    const year = yearFromQuery(req.query.year);
    const rows = await prisma.leaveAllocation.findMany({
      where: { year },
      orderBy: [{ employeeId: 'asc' }, { typeId: 'asc' }],
    });
    res.json({ year, allocations: rows.map(serializeAllocation) });
  } catch (err) {
    next(err);
  }
});

const allocPutSchema = z.object({
  year: z.number().int().min(2000).max(2100),
  employeeId: z.string().uuid(),
  allocations: z.record(z.string(), z.number().min(0)),
});

leaveRouter.put('/allocations', async (req: AuthedRequest, res, next) => {
  try {
    requireSuperAdmin(req);
    assertCan(req.user!, 'manage_leave');
    const body = allocPutSchema.parse(req.body || {});
    const validUntil = new Date(`${body.year}-12-31T00:00:00.000Z`);
    const typeIds = ['cl', 'el', 'sl', 'comp'];
    const results = [];

    for (const typeId of typeIds) {
      const allocated = Math.max(0, Number(body.allocations[typeId] ?? 0));
      const row = await prisma.leaveAllocation.upsert({
        where: {
          employeeId_typeId_year: {
            employeeId: body.employeeId,
            typeId,
            year: body.year,
          },
        },
        create: {
          employeeId: body.employeeId,
          typeId,
          year: body.year,
          allocated,
          validUntil,
        },
        update: {
          allocated,
          validUntil,
        },
      });
      results.push(serializeAllocation(row));
    }

    res.json({ allocations: results });
  } catch (err) {
    next(err);
  }
});

leaveRouter.get('/holidays', async (req: AuthedRequest, res, next) => {
  try {
    const year = req.query.year != null && req.query.year !== ''
      ? yearFromQuery(req.query.year)
      : null;
    const rows = await prisma.holiday.findMany({
      orderBy: { date: 'asc' },
    });
    const filtered = year == null
      ? rows
      : rows.filter((h) => h.date.getUTCFullYear() === year);
    res.json({ holidays: filtered.map(serializeHoliday) });
  } catch (err) {
    next(err);
  }
});

const holidaySchema = z.object({
  date: z.string().min(1),
  name: z.string().min(1),
});

leaveRouter.post('/holidays', async (req: AuthedRequest, res, next) => {
  try {
    requireSuperAdmin(req);
    assertCan(req.user!, 'manage_leave');
    const body = holidaySchema.parse(req.body || {});
    const date = toDateOnly(body.date);
    if (!date) throw badRequest('Invalid date');
    const existing = await prisma.holiday.findUnique({ where: { date } });
    if (existing) throw badRequest('A holiday already exists on that date');
    const row = await prisma.holiday.create({
      data: { date, name: body.name.trim() },
    });
    res.status(201).json({ holiday: serializeHoliday(row) });
  } catch (err) {
    next(err);
  }
});

leaveRouter.delete('/holidays/:id', async (req: AuthedRequest, res, next) => {
  try {
    requireSuperAdmin(req);
    assertCan(req.user!, 'manage_leave');
    const id = param(req.params.id);
    const existing = await prisma.holiday.findUnique({ where: { id } });
    if (!existing) throw notFound('Holiday not found');
    await prisma.holiday.delete({ where: { id } });
    res.json({ ok: true });
  } catch (err) {
    next(err);
  }
});

// Expose helpers for tests / debug if needed
export { leaveDaysBetween, normalizeDayPeriod };
