import { Router } from 'express';
import {
  BillingType,
  Prisma,
  ProjectMemberRole,
  SystemRole,
  TimesheetApproval,
} from '@prisma/client';
import { z } from 'zod';
import { prisma } from '../db';
import { parseHhmm, toDateOnly } from '../lib/dates';
import { badRequest, forbidden, notFound } from '../lib/errors';
import { isUuid, param } from '../lib/params';
import { parseListLimit, parseListOffset } from '../lib/pagination';
import {
  AuthUser,
  assertCan,
  loadProjectContext,
  visibleProjectIds,
} from '../lib/rbac';
import { AuthedRequest, requireAuth } from '../middleware/auth';
import { billingToDb } from '../serializers/task';
import {
  categoryToDb,
  serializeTimesheet,
} from '../serializers/timesheet';

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

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

const include = {
  employee: { select: empSelect },
  project: { select: { id: true, code: true, name: true } },
  task: { select: { id: true, code: true, title: true, billingType: true } },
  bug: { select: { id: true, code: true, title: true } },
} satisfies Prisma.TimesheetInclude;

function isAdminRole(user: AuthUser) {
  return user.role === SystemRole.SuperAdmin || user.role === SystemRole.ProjectAdmin;
}

async function canApproveOnProject(user: AuthUser, projectId: string) {
  if (user.role === SystemRole.SuperAdmin) return true;
  const ctx = await loadProjectContext(user.id, projectId);
  return (
    user.role === SystemRole.ProjectAdmin ||
    ctx.projectAdminId === user.id ||
    ctx.projectRole === ProjectMemberRole.ProjectAdmin
  );
}

function parseHours(raw: unknown): number {
  if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {
    return Math.round(raw * 100) / 100;
  }
  if (typeof raw === 'string') {
    const hhmm = parseHhmm(raw);
    if (!Number.isNaN(hhmm) && hhmm > 0) return hhmm;
    const n = Number(raw);
    if (Number.isFinite(n) && n > 0) return Math.round(n * 100) / 100;
  }
  throw badRequest('Hours must be HH:MM or a positive decimal');
}

async function resolveTaskBug(body: {
  taskId?: string | null;
  bugId?: string | null;
}) {
  let taskId = body.taskId || null;
  let bugId = body.bugId || null;
  let projectId: string | null = null;
  let billingType: BillingType = BillingType.Billable;

  if (bugId) {
    const bug = await prisma.bug.findFirst({
      where: isUuid(bugId) ? { id: bugId } : { code: bugId },
      include: { parentTask: true },
    });
    if (!bug) throw badRequest('Bug not found');
    bugId = bug.id;
    taskId = bug.parentTaskId;
    projectId = bug.parentTask.projectId;
    billingType = bug.parentTask.billingType;
  } else if (taskId) {
    const task = await prisma.task.findFirst({
      where: isUuid(taskId) ? { id: taskId } : { code: taskId },
    });
    if (!task) throw badRequest('Task not found');
    taskId = task.id;
    projectId = task.projectId;
    billingType = task.billingType;
  } else {
    throw badRequest('taskId or bugId is required');
  }

  return { taskId, bugId, projectId: projectId!, billingType };
}

const writeSchema = z.object({
  employeeId: z.string().uuid().optional(),
  taskId: z.string().optional().nullable(),
  bugId: z.string().optional().nullable(),
  workDate: z.string().min(1),
  hours: z.union([z.number(), z.string()]),
  category: z.string().min(1),
  description: z.string().optional(),
});

timesheetsRouter.get('/', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    const q = String(req.query.q || '').trim().toLowerCase();
    const projectId = req.query.projectId ? String(req.query.projectId) : '';
    const clientId = req.query.clientId ? String(req.query.clientId) : '';
    const employeeId = req.query.employeeId ? String(req.query.employeeId) : '';
    const taskId = req.query.taskId ? String(req.query.taskId) : '';
    const bugId = req.query.bugId ? String(req.query.bugId) : '';
    const billingType = req.query.billingType ? String(req.query.billingType) : '';
    const from = req.query.from ? String(req.query.from) : '';
    const to = req.query.to ? String(req.query.to) : '';
    const includeSubtasks = String(req.query.includeSubtasks || '') === '1';

    const visible = await visibleProjectIds(user);
    const where: Prisma.TimesheetWhereInput = {};

    if (visible !== 'all') {
      where.projectId = { in: visible.length ? visible : ['00000000-0000-0000-0000-000000000000'] };
    }

    if (!isAdminRole(user)) {
      where.employeeId = user.id;
    } else if (employeeId) {
      where.employeeId = employeeId;
    }

    if (projectId) where.projectId = projectId;
    if (clientId) where.project = { clientId };

    if (bugId) {
      where.bugId = bugId;
    } else if (taskId) {
      if (includeSubtasks) {
        const children = await prisma.task.findMany({
          where: { parentId: taskId },
          select: { id: true },
        });
        const ids = [taskId, ...children.map((c) => c.id)];
        where.taskId = { in: ids };
      } else {
        where.taskId = taskId;
      }
    }

    if (billingType) {
      where.billingType = billingToDb(billingType);
    }

    const fromDate = from ? toDateOnly(from) : null;
    const toDate = to ? toDateOnly(to) : null;
    if (fromDate || toDate) {
      where.workDate = {};
      if (fromDate) where.workDate.gte = fromDate;
      if (toDate) where.workDate.lte = toDate;
    }

    const limit = parseListLimit(req.query.limit);
    const offset = parseListOffset(req.query.offset);

    let rows = await prisma.timesheet.findMany({
      where,
      include,
      orderBy: [{ workDate: 'desc' }, { createdAt: 'desc' }],
      take: limit + 1 + (q ? 500 : 0),
      skip: offset,
    });

    if (q) {
      rows = rows.filter((r) => {
        const hay = [
          r.description,
          r.category,
          r.employee?.name,
          r.project?.code,
          r.project?.name,
          r.task?.code,
          r.task?.title,
          r.bug?.code,
          r.bug?.title,
        ]
          .join(' ')
          .toLowerCase();
        return hay.includes(q);
      });
    }

    const hasMore = rows.length > limit;
    const page = hasMore ? rows.slice(0, limit) : rows;
    res.json({
      timesheets: page.map(serializeTimesheet),
      limit,
      offset,
      hasMore,
    });
  } catch (err) {
    next(err);
  }
});

timesheetsRouter.post('/', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    assertCan(user, 'log_time');
    const body = writeSchema.parse(req.body);
    const resolved = await resolveTaskBug(body);
    const pctx = await loadProjectContext(user.id, resolved.projectId);
    if (!pctx.isProjectMember && user.role !== SystemRole.SuperAdmin) {
      throw forbidden('Not a member of this project');
    }

    let employeeId = body.employeeId || user.id;
    if (employeeId !== user.id && !isAdminRole(user)) {
      throw forbidden('Cannot log time for another employee');
    }

    const workDate = toDateOnly(body.workDate);
    if (!workDate) throw badRequest('Invalid workDate');
    const hours = parseHours(body.hours);

    const row = await prisma.timesheet.create({
      data: {
        employeeId,
        projectId: resolved.projectId,
        taskId: resolved.taskId,
        bugId: resolved.bugId,
        workDate,
        hours,
        category: categoryToDb(body.category),
        description: body.description || '',
        billingType: resolved.billingType,
        approvalStatus: TimesheetApproval.Pending,
      },
      include,
    });

    res.status(201).json({ timesheet: serializeTimesheet(row) });
  } catch (err) {
    next(err);
  }
});

timesheetsRouter.patch('/:id', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    assertCan(user, 'log_time');
    const id = param(req.params.id);
    const existing = await prisma.timesheet.findUnique({ where: { id } });
    if (!existing) throw notFound('Timesheet not found');

    if (existing.employeeId !== user.id && !isAdminRole(user)) {
      throw forbidden('Cannot edit another employee\'s timesheet');
    }

    const body = writeSchema.partial().parse(req.body);
    const data: Prisma.TimesheetUpdateInput = {};

    if (body.taskId !== undefined || body.bugId !== undefined) {
      const resolved = await resolveTaskBug({
        taskId: body.taskId !== undefined ? body.taskId : existing.taskId,
        bugId: body.bugId !== undefined ? body.bugId : existing.bugId,
      });
      const pctx = await loadProjectContext(user.id, resolved.projectId);
      if (!pctx.isProjectMember && user.role !== SystemRole.SuperAdmin) {
        throw forbidden('Not a member of this project');
      }
      data.project = { connect: { id: resolved.projectId } };
      data.task = resolved.taskId ? { connect: { id: resolved.taskId } } : { disconnect: true };
      data.bug = resolved.bugId ? { connect: { id: resolved.bugId } } : { disconnect: true };
      data.billingType = resolved.billingType;
    }

    if (body.employeeId !== undefined) {
      if (body.employeeId !== user.id && !isAdminRole(user)) {
        throw forbidden('Cannot reassign timesheet to another employee');
      }
      data.employee = { connect: { id: body.employeeId } };
    }
    if (body.workDate !== undefined) {
      const workDate = toDateOnly(body.workDate);
      if (!workDate) throw badRequest('Invalid workDate');
      data.workDate = workDate;
    }
    if (body.hours !== undefined) data.hours = parseHours(body.hours);
    if (body.category !== undefined) data.category = categoryToDb(body.category);
    if (body.description !== undefined) data.description = body.description;

    const row = await prisma.timesheet.update({
      where: { id },
      data,
      include,
    });
    res.json({ timesheet: serializeTimesheet(row) });
  } catch (err) {
    next(err);
  }
});

timesheetsRouter.delete('/:id', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    assertCan(user, 'log_time');
    const id = param(req.params.id);
    const existing = await prisma.timesheet.findUnique({ where: { id } });
    if (!existing) throw notFound('Timesheet not found');
    if (existing.employeeId !== user.id && !isAdminRole(user)) {
      throw forbidden('Cannot delete another employee\'s timesheet');
    }
    await prisma.timesheet.delete({ where: { id } });
    res.json({ ok: true });
  } catch (err) {
    next(err);
  }
});

timesheetsRouter.post('/:id/approve', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    const id = param(req.params.id);
    const existing = await prisma.timesheet.findUnique({ where: { id } });
    if (!existing) throw notFound('Timesheet not found');
    if (!(await canApproveOnProject(user, existing.projectId))) {
      throw forbidden('Only Super Admin or Project Admin can approve timesheets');
    }
    const row = await prisma.timesheet.update({
      where: { id },
      data: { approvalStatus: TimesheetApproval.Approved },
      include,
    });
    res.json({ timesheet: serializeTimesheet(row) });
  } catch (err) {
    next(err);
  }
});
