import { Router } from 'express';
import {
  BillingType,
  Prisma,
  TimerEntityType,
  TimerStatus,
  TimesheetApproval,
  TimesheetCategory,
} from '@prisma/client';
import { z } from 'zod';
import { prisma } from '../db';
import { hoursToHhmm } from '../lib/dates';
import { badRequest, forbidden, notFound } from '../lib/errors';
import { isUuid } from '../lib/params';
import { assertCan, loadProjectContext } from '../lib/rbac';
import { AuthedRequest, requireAuth } from '../middleware/auth';
import {
  categoryFromTimesheetDb,
  categoryToDb,
  serializeTimesheet,
} from '../serializers/timesheet';

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

const STALE_MS = 2 * 60 * 1000;

type TimerRow = Prisma.ActiveTimerGetPayload<object>;

function todayLocalKey(d = new Date()): string {
  const yyyy = d.getFullYear();
  const mm = String(d.getMonth() + 1).padStart(2, '0');
  const dd = String(d.getDate()).padStart(2, '0');
  return `${yyyy}-${mm}-${dd}`;
}

function dayKey(value: Date | string | null | undefined): string {
  if (!value) return todayLocalKey();
  if (value instanceof Date) {
    return todayLocalKey(value);
  }
  const s = String(value);
  const m = s.match(/^(\d{4}-\d{2}-\d{2})/);
  if (m) return m[1];
  const d = new Date(s);
  if (Number.isNaN(d.getTime())) return todayLocalKey();
  return todayLocalKey(d);
}

function endOfLocalDay(dateStr: string): Date {
  return new Date(dateStr + 'T23:59:59.000');
}

function elapsedMs(timer: TimerRow, endAt?: Date | null): number {
  let ms = Number(timer.accumulatedMs || 0);
  if (timer.status === TimerStatus.running && timer.startedAt) {
    const end = endAt ? endAt.getTime() : Date.now();
    const start = timer.startedAt.getTime();
    if (!Number.isNaN(end) && !Number.isNaN(start)) {
      ms += Math.max(0, end - start);
    }
  }
  return ms;
}

function formatElapsed(ms: number): string {
  const total = Math.max(0, Math.floor((ms || 0) / 1000));
  const h = Math.floor(total / 3600);
  const m = Math.floor((total % 3600) / 60);
  const s = total % 60;
  return (
    String(h).padStart(2, '0') +
    ':' +
    String(m).padStart(2, '0') +
    ':' +
    String(s).padStart(2, '0')
  );
}

async function loadEntityLabel(timer: TimerRow) {
  if (timer.bugId) {
    const bug = await prisma.bug.findUnique({
      where: { id: timer.bugId },
      select: { code: true, title: true },
    });
    return bug ? `${bug.code} — ${bug.title}` : timer.bugId;
  }
  if (timer.taskId) {
    const task = await prisma.task.findUnique({
      where: { id: timer.taskId },
      select: { code: true, title: true, parentId: true },
    });
    if (!task) return timer.taskId;
    const kind = task.parentId ? 'Subtask' : 'Task';
    return `${kind} ${task.code} — ${task.title}`;
  }
  return '';
}

function serializeTimer(timer: TimerRow, label?: string) {
  return {
    id: timer.id,
    employeeId: timer.employeeId,
    entityType: timer.entityType,
    taskId: timer.taskId,
    bugId: timer.bugId,
    projectId: timer.projectId,
    status: timer.status,
    startedAt: timer.startedAt.toISOString(),
    accumulatedMs: timer.accumulatedMs,
    category: categoryFromTimesheetDb(timer.category),
    description: timer.description,
    sessionDate: timer.sessionDate.toISOString().slice(0, 10),
    lastHeartbeatAt: timer.lastHeartbeatAt.toISOString(),
    elapsedMs: elapsedMs(timer),
    label: label || '',
  };
}

async function resolveTarget(opts: { taskId?: string; bugId?: string }) {
  if (opts.bugId) {
    const bug = await prisma.bug.findFirst({
      where: isUuid(opts.bugId) ? { id: opts.bugId } : { code: opts.bugId },
      include: { parentTask: true },
    });
    if (!bug) throw badRequest('Bug not found');
    return {
      entityType: TimerEntityType.bug,
      bugId: bug.id,
      taskId: bug.parentTaskId,
      projectId: bug.parentTask.projectId,
      category: TimesheetCategory.BugFixing,
      billingType: bug.parentTask.billingType,
      title: bug.title,
    };
  }
  if (!opts.taskId) throw badRequest('taskId or bugId required');
  const task = await prisma.task.findFirst({
    where: isUuid(opts.taskId) ? { id: opts.taskId } : { code: opts.taskId },
  });
  if (!task) throw badRequest('Task not found');
  return {
    entityType: TimerEntityType.task,
    bugId: null as string | null,
    taskId: task.id,
    projectId: task.projectId,
    category: TimesheetCategory.Development,
    billingType: task.billingType,
    title: task.title,
  };
}

async function createTimesheetFromTimer(
  timer: TimerRow,
  opts: {
    description?: string;
    autoClosed?: boolean;
    endAt?: Date | null;
    workDate?: string;
    category?: string;
    force?: boolean;
    silentUnderMinute?: boolean;
  }
) {
  const ms = elapsedMs(timer, opts.endAt || null);
  const minutes = Math.round(ms / 60000);
  const hours = Math.round((minutes / 60) * 100) / 100;

  if (!opts.force && minutes < 1) {
    await prisma.activeTimer.delete({ where: { id: timer.id } });
    return { discarded: true as const, underMinute: true as const, timesheet: null };
  }

  const logHours = minutes < 1 ? 0.02 : hours;
  let billingType: BillingType = BillingType.Billable;
  if (timer.taskId) {
    const task = await prisma.task.findUnique({ where: { id: timer.taskId } });
    if (task) billingType = task.billingType;
  }

  const category = opts.category
    ? categoryToDb(opts.category)
    : timer.category ||
      (timer.bugId ? TimesheetCategory.BugFixing : TimesheetCategory.Development);

  const descriptionBase =
    String(
      opts.description ||
        (opts.autoClosed ? 'Auto-stopped' : timer.description || 'Timed work')
    ).trim() || 'Timed work';
  const description = `${descriptionBase} (timer ${formatElapsed(ms)})`;

  const workDateStr =
    opts.workDate || dayKey(timer.sessionDate) || dayKey(timer.lastHeartbeatAt);
  const workDate = new Date(workDateStr + 'T00:00:00.000Z');

  const [timesheet] = await prisma.$transaction([
    prisma.timesheet.create({
      data: {
        employeeId: timer.employeeId,
        projectId: timer.projectId,
        taskId: timer.taskId,
        bugId: timer.bugId,
        workDate,
        hours: logHours,
        category,
        description,
        billingType,
        approvalStatus: TimesheetApproval.Pending,
        fromTimer: true,
        autoClosed: !!opts.autoClosed,
      },
      include: {
        employee: { select: { id: true, name: true, code: true, avatarColor: true } },
        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 } },
      },
    }),
    prisma.activeTimer.delete({ where: { id: timer.id } }),
  ]);

  return {
    discarded: false as const,
    underMinute: false as const,
    timesheet,
    hours: logHours,
    workDate: workDateStr,
  };
}

type RecoverResult = Awaited<ReturnType<typeof createTimesheetFromTimer>>;

async function recoverIfNeeded(timer: TimerRow | null): Promise<{
  timer: TimerRow | null;
  recovered: RecoverResult | null;
}> {
  if (!timer) return { timer: null, recovered: null };

  const today = todayLocalKey();
  const sessionDay = dayKey(timer.sessionDate);
  const heartbeatDay = dayKey(timer.lastHeartbeatAt || timer.startedAt);
  const staleMs = timer.lastHeartbeatAt
    ? Date.now() - timer.lastHeartbeatAt.getTime()
    : Date.now() - timer.startedAt.getTime();

  const crossedDay = sessionDay < today || heartbeatDay < today;
  const staleSession = staleMs > STALE_MS;

  if (!crossedDay && !staleSession && timer.status === TimerStatus.running) {
    const updated = await prisma.activeTimer.update({
      where: { id: timer.id },
      data: { lastHeartbeatAt: new Date() },
    });
    return { timer: updated, recovered: null };
  }
  if (!crossedDay && !staleSession && timer.status === TimerStatus.paused) {
    return { timer, recovered: null };
  }

  let endAt: Date | null = timer.lastHeartbeatAt || null;
  if (!endAt && timer.status === TimerStatus.running && timer.startedAt) {
    const startDay = dayKey(timer.startedAt);
    endAt = startDay < today ? endOfLocalDay(startDay) : timer.startedAt;
  }
  if (endAt && dayKey(endAt) < today && timer.status === TimerStatus.running) {
    endAt = endOfLocalDay(dayKey(endAt));
  }

  const recovered = await createTimesheetFromTimer(timer, {
    force: true,
    autoClosed: true,
    description: crossedDay
      ? 'Auto-stopped after previous day/session'
      : 'Auto-stopped after browser was closed',
    workDate: sessionDay || heartbeatDay || today,
    endAt,
  });

  return { timer: null, recovered };
}

timerRouter.get('/', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    const existing = await prisma.activeTimer.findUnique({
      where: { employeeId: user.id },
    });
    const { timer, recovered } = await recoverIfNeeded(existing);
    const label = timer ? await loadEntityLabel(timer) : '';
    res.json({
      timer: timer ? serializeTimer(timer, label) : null,
      recovered: recovered && !recovered.discarded && recovered.timesheet
        ? {
            timesheet: serializeTimesheet(recovered.timesheet),
            hours: recovered.hours,
            workDate: recovered.workDate,
            message:
              'Previous timer auto-logged ' +
              hoursToHhmm(recovered.hours!) +
              ' on ' +
              recovered.workDate,
          }
        : null,
    });
  } catch (err) {
    next(err);
  }
});

const startSchema = z.object({
  taskId: z.string().optional(),
  bugId: z.string().optional(),
  switch: z.boolean().optional(),
});

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

    let existing = await prisma.activeTimer.findUnique({
      where: { employeeId: user.id },
    });
    const recovered = await recoverIfNeeded(existing);
    existing = recovered.timer;

    if (existing) {
      const same =
        target.bugId
          ? existing.bugId === target.bugId
          : existing.taskId === target.taskId && !existing.bugId;
      if (same) {
        if (existing.status === TimerStatus.paused) {
          const resumed = await prisma.activeTimer.update({
            where: { id: existing.id },
            data: {
              status: TimerStatus.running,
              startedAt: new Date(),
              lastHeartbeatAt: new Date(),
            },
          });
          const label = await loadEntityLabel(resumed);
          return res.json({
            timer: serializeTimer(resumed, label),
            switched: false,
            resumed: true,
          });
        }
        const label = await loadEntityLabel(existing);
        return res.json({
          timer: serializeTimer(existing, label),
          switched: false,
          alreadyRunning: true,
        });
      }
      if (!body.switch) {
        const label = await loadEntityLabel(existing);
        return res.status(409).json({
          error: {
            message: 'Another timer is active',
            code: 'timer_active',
            details: { timer: serializeTimer(existing, label) },
          },
        });
      }
      await createTimesheetFromTimer(existing, {
        force: true,
        description: 'Switched timer',
        silentUnderMinute: true,
      });
    }

    const now = new Date();
    const timer = await prisma.activeTimer.create({
      data: {
        employeeId: user.id,
        entityType: target.entityType,
        taskId: target.taskId,
        bugId: target.bugId,
        projectId: target.projectId,
        status: TimerStatus.running,
        startedAt: now,
        accumulatedMs: 0,
        category: target.category,
        description: '',
        sessionDate: new Date(todayLocalKey() + 'T00:00:00.000Z'),
        lastHeartbeatAt: now,
      },
    });
    const label = await loadEntityLabel(timer);
    res.status(201).json({
      timer: serializeTimer(timer, label),
      switched: Boolean(existing),
      recovered: recovered.recovered && !recovered.recovered.discarded
        ? serializeTimesheet(recovered.recovered.timesheet!)
        : null,
    });
  } catch (err) {
    next(err);
  }
});

timerRouter.post('/pause', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    const timer = await prisma.activeTimer.findUnique({
      where: { employeeId: user.id },
    });
    if (!timer) throw notFound('No active timer');
    if (timer.status !== TimerStatus.running) {
      throw badRequest('Timer is not running');
    }
    const add = Math.max(0, Date.now() - timer.startedAt.getTime());
    const updated = await prisma.activeTimer.update({
      where: { id: timer.id },
      data: {
        accumulatedMs: timer.accumulatedMs + add,
        status: TimerStatus.paused,
        lastHeartbeatAt: new Date(),
      },
    });
    const label = await loadEntityLabel(updated);
    res.json({ timer: serializeTimer(updated, label) });
  } catch (err) {
    next(err);
  }
});

timerRouter.post('/resume', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    const timer = await prisma.activeTimer.findUnique({
      where: { employeeId: user.id },
    });
    if (!timer) throw notFound('No active timer');
    if (timer.status !== TimerStatus.paused) {
      throw badRequest('Timer is not paused');
    }
    const updated = await prisma.activeTimer.update({
      where: { id: timer.id },
      data: {
        status: TimerStatus.running,
        startedAt: new Date(),
        lastHeartbeatAt: new Date(),
      },
    });
    const label = await loadEntityLabel(updated);
    res.json({ timer: serializeTimer(updated, label) });
  } catch (err) {
    next(err);
  }
});

timerRouter.post('/heartbeat', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    const timer = await prisma.activeTimer.findUnique({
      where: { employeeId: user.id },
    });
    if (!timer) {
      return res.json({ timer: null });
    }
    const updated = await prisma.activeTimer.update({
      where: { id: timer.id },
      data: { lastHeartbeatAt: new Date() },
    });
    res.json({ timer: serializeTimer(updated) });
  } catch (err) {
    next(err);
  }
});

const stopSchema = z.object({
  description: z.string().optional(),
  category: z.string().optional(),
  autoClosed: z.boolean().optional(),
  force: z.boolean().optional(),
  workDate: z.string().optional(),
  endAt: z.string().optional(),
});

timerRouter.post('/stop', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    assertCan(user, 'log_time');
    const body = stopSchema.parse(req.body || {});
    const timer = await prisma.activeTimer.findUnique({
      where: { employeeId: user.id },
    });
    if (!timer) throw notFound('No active timer');

    const endAt = body.endAt ? new Date(body.endAt) : null;
    const result = await createTimesheetFromTimer(timer, {
      description: body.description,
      category: body.category,
      autoClosed: body.autoClosed,
      force: body.force,
      workDate: body.workDate,
      endAt: endAt && !Number.isNaN(endAt.getTime()) ? endAt : null,
    });

    if (result.discarded) {
      return res.json({
        ok: true,
        discarded: true,
        underMinute: true,
        timesheet: null,
      });
    }

    res.json({
      ok: true,
      discarded: false,
      timesheet: serializeTimesheet(result.timesheet!),
      hours: result.hours,
    });
  } catch (err) {
    next(err);
  }
});

timerRouter.post('/discard', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    const timer = await prisma.activeTimer.findUnique({
      where: { employeeId: user.id },
    });
    if (!timer) throw notFound('No active timer');
    await prisma.activeTimer.delete({ where: { id: timer.id } });
    res.json({ ok: true });
  } catch (err) {
    next(err);
  }
});
