import { Router } from 'express';
import { Prisma, ProjectStatus, SystemRole } from '@prisma/client';
import { z } from 'zod';
import { prisma } from '../db';
import { DEFAULT_INTERNAL_STATUSES } from '../lib/defaultInternalStatuses';
import {
  DELIVERY_RULE_DEFAULTS,
  DELIVERY_RULE_KEYS,
  DeliveryRuleKey,
  pickDeliveryRules,
} from '../lib/deliveryRules';
import { badRequest, conflict, forbidden, notFound } from '../lib/errors';
import { nextProjectHumanCode } from '../lib/ids';
import { toDateOnly } from '../lib/dates';
import { param } from '../lib/params';
import {
  assertCan,
  can,
  loadProjectContext,
  visibleProjectIds,
} from '../lib/rbac';
import { PROJECT_ROLES, projectRoleToDb } from '../lib/roles';
import { AuthedRequest, requireAuth } from '../middleware/auth';
import { serializeProject } from '../serializers/project';
import { assertAssignableClient } from './clients';

const deliveryRuleSchema = Object.fromEntries(
  DELIVERY_RULE_KEYS.map((k) => [k, z.boolean().optional()])
) as Record<DeliveryRuleKey, z.ZodOptional<z.ZodBoolean>>;

export const projectsRouter = Router();

projectsRouter.use(requireAuth);

const statusSchema = z.enum(['Active', 'On Hold', 'Completed']);
const memberSchema = z.object({
  employeeId: z.string().uuid(),
  projectRole: z.enum(PROJECT_ROLES),
});

const createSchema = z.object({
  code: z
    .string()
    .trim()
    .min(2)
    .max(32)
    .regex(/^[A-Za-z0-9_-]+$/, 'Code must be alphanumeric'),
  name: z.string().trim().min(1),
  description: z.string().optional().default(''),
  startDate: z.string().optional().nullable(),
  endDate: z.string().optional().nullable(),
  adminId: z.string().uuid(),
  status: statusSchema.optional().default('Active'),
  color: z.string().optional().default('#3C8DBC'),
  tags: z.array(z.string()).optional().default([]),
  clientId: z.string().uuid().nullable().optional(),
  ...deliveryRuleSchema,
  members: z.array(memberSchema).optional().default([]),
});

const updateSchema = createSchema.partial();

function statusToDb(s: string): ProjectStatus {
  if (s === 'On Hold') return ProjectStatus.OnHold;
  if (s === 'Completed') return ProjectStatus.Completed;
  return ProjectStatus.Active;
}

const projectInclude = {
  members: { include: { employee: { select: { id: true, name: true, code: true } } } },
  internalStatuses: true,
  admin: { select: { id: true, name: true, code: true } },
  client: { select: { id: true, code: true, name: true } },
  _count: { select: { tasks: true } },
} satisfies Prisma.ProjectInclude;

projectsRouter.get('/', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    const q = String(req.query.q || '').trim();
    const ids = await visibleProjectIds(user);
    const where: Prisma.ProjectWhereInput = {};
    if (ids !== 'all') where.id = { in: ids };
    const clientId = req.query.clientId ? String(req.query.clientId) : '';
    if (clientId) where.clientId = clientId;
    if (q) {
      where.AND = [
        {
          OR: [
            { name: { contains: q, mode: 'insensitive' } },
            { code: { contains: q, mode: 'insensitive' } },
            { description: { contains: q, mode: 'insensitive' } },
          ],
        },
      ];
    }

    const rows = await prisma.project.findMany({
      where,
      include: projectInclude,
      orderBy: { name: 'asc' },
    });
    res.json({ projects: rows.map(serializeProject) });
  } catch (err) {
    next(err);
  }
});

projectsRouter.get('/:id', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    const id = param(req.params.id);
    const project = await prisma.project.findFirst({
      where: {
        OR: [{ id }, { code: id }, { humanCode: id }],
      },
      include: projectInclude,
    });
    if (!project) throw notFound('Project not found');

    const ids = await visibleProjectIds(user);
    if (ids !== 'all' && !ids.includes(project.id)) {
      throw forbidden('You cannot view this project');
    }

    const stageCounts = await prisma.task.groupBy({
      by: ['stage'],
      where: { projectId: project.id, parentId: null },
      _count: true,
    });

    res.json({
      project: serializeProject(project),
      stageCounts: Object.fromEntries(
        stageCounts.map((s) => [s.stage, s._count])
      ),
    });
  } catch (err) {
    next(err);
  }
});

projectsRouter.post('/', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    if (!can(user, 'create_project') && user.role !== SystemRole.SuperAdmin) {
      throw forbidden('Only Project Admin or Super Admin can create projects');
    }

    const body = createSchema.parse(req.body);
    const code = body.code.toUpperCase();
    const exists = await prisma.project.findUnique({ where: { code } });
    if (exists) throw conflict(`Project code ${code} already exists`);

    const admin = await prisma.employee.findUnique({ where: { id: body.adminId } });
    if (!admin || !admin.active) throw badRequest('Admin employee not found or inactive');
    await assertAssignableClient(body.clientId);

    const humanCode = await nextProjectHumanCode();
    const memberIds = new Set(body.members.map((m) => m.employeeId));
    memberIds.add(body.adminId);

    const membersData = [...memberIds].map((employeeId) => {
      const fromBody = body.members.find((m) => m.employeeId === employeeId);
      const roleLabel =
        fromBody?.projectRole ||
        (employeeId === body.adminId ? 'Project Admin' : 'Member');
      return {
        employeeId,
        projectRole: projectRoleToDb(roleLabel),
      };
    });

    const project = await prisma.project.create({
      data: {
        code,
        humanCode,
        name: body.name,
        description: body.description || '',
        startDate: toDateOnly(body.startDate),
        endDate: toDateOnly(body.endDate),
        adminId: body.adminId,
        status: statusToDb(body.status),
        color: body.color || '#3C8DBC',
        tags: body.tags || [],
        clientId: body.clientId || null,
        ...pickDeliveryRules({
          ...DELIVERY_RULE_DEFAULTS,
          ...Object.fromEntries(
            DELIVERY_RULE_KEYS.map((k) => [k, body[k]])
          ),
        }),
        members: { create: membersData },
        internalStatuses: {
          create: DEFAULT_INTERNAL_STATUSES.map((s) => ({
            text: s.text,
            color: s.color,
            stage: s.stage,
            sortOrder: s.sortOrder,
            active: true,
          })),
        },
      },
      include: projectInclude,
    });

    res.status(201).json({ project: serializeProject(project) });
  } catch (err) {
    next(err);
  }
});

projectsRouter.patch('/:id', async (req: AuthedRequest, res, next) => {
  try {
    const user = req.user!;
    const id = param(req.params.id);
    const project = await prisma.project.findFirst({
      where: {
        OR: [{ id }, { code: id }, { humanCode: id }],
      },
    });
    if (!project) throw notFound('Project not found');

    const ctx = await loadProjectContext(user.id, project.id);
    assertCan(user, 'manage_project', { ...ctx, isProjectMember: true });

    const body = updateSchema.parse(req.body);
    if (body.code && body.code.toUpperCase() !== project.code) {
      const code = body.code.toUpperCase();
      const clash = await prisma.project.findUnique({ where: { code } });
      if (clash) throw conflict(`Project code ${code} already exists`);
    }

    if (body.adminId) {
      const admin = await prisma.employee.findUnique({ where: { id: body.adminId } });
      if (!admin || !admin.active) throw badRequest('Admin employee not found or inactive');
    }
    if (body.clientId !== undefined) {
      await assertAssignableClient(body.clientId, project.clientId);
    }

    await prisma.$transaction(async (tx) => {
      // Use UncheckedUpdateInput so scalar FKs (adminId) + delivery flags are accepted.
      const data: Prisma.ProjectUncheckedUpdateInput = {
        code: body.code ? body.code.toUpperCase() : undefined,
        name: body.name,
        description: body.description,
        startDate: body.startDate !== undefined ? toDateOnly(body.startDate) : undefined,
        endDate: body.endDate !== undefined ? toDateOnly(body.endDate) : undefined,
        adminId: body.adminId,
        status: body.status ? statusToDb(body.status) : undefined,
        color: body.color,
        tags: body.tags,
        clientId: body.clientId === undefined ? undefined : body.clientId,
      };
      for (const key of DELIVERY_RULE_KEYS) {
        if (body[key] !== undefined) data[key] = body[key];
      }
      await tx.project.update({
        where: { id: project.id },
        data,
      });

      if (body.members) {
        await tx.projectMember.deleteMany({ where: { projectId: project.id } });
        const adminId = body.adminId || project.adminId;
        const memberIds = new Set(body.members.map((m) => m.employeeId));
        memberIds.add(adminId);
        await tx.projectMember.createMany({
          data: [...memberIds].map((employeeId) => {
            const fromBody = body.members!.find((m) => m.employeeId === employeeId);
            const roleLabel =
              fromBody?.projectRole ||
              (employeeId === adminId ? 'Project Admin' : 'Member');
            return {
              projectId: project.id,
              employeeId,
              projectRole: projectRoleToDb(roleLabel),
            };
          }),
        });
      }
    });

    const updated = await prisma.project.findUniqueOrThrow({
      where: { id: project.id },
      include: projectInclude,
    });
    res.json({ project: serializeProject(updated) });
  } catch (err) {
    next(err);
  }
});
