import { Prisma } from '@prisma/client';
import prisma from '../lib/prisma';
import { AppError } from '../middleware/errorHandler';
import {
  BOOKING,
  APPOINTMENT_TRANSITIONS,
  AppointmentStatus,
  CreateAppointmentInput,
} from '@nobel/shared';
import { nextAppointmentNo, nextOrderNo } from './sequences.service';
import { generatePortalToken } from './portal.service';

type TxClient = Prisma.TransactionClient;

// All dates are handled as YYYY-MM-DD strings; the DB column is DATE, which
// Prisma round-trips as midnight-UTC Date objects. Always construct with
// new Date(dateStr) and read back with toDateOnly to avoid TZ drift.
export function toDateOnly(d: Date): string {
  return d.toISOString().slice(0, 10);
}

// "Today" in server-local time (the shop's timezone), NOT UTC — using
// toISOString here would offer yesterday's slots to early-morning visitors.
export function localToday(): string {
  const now = new Date();
  return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
}

function isOpenDay(dateStr: string): boolean {
  // Parse as UTC so getUTCDay matches the stored DATE
  return (BOOKING.openDays as readonly number[]).includes(new Date(dateStr + 'T00:00:00Z').getUTCDay());
}

export async function getAvailability(fromStr: string, days: number) {
  const shop = await prisma.shop.findFirst();
  const capacity = shop?.slotCapacity ?? 2;

  const from = new Date(fromStr + 'T00:00:00Z');
  const to = new Date(from);
  to.setUTCDate(to.getUTCDate() + days);

  const counts = await prisma.appointment.groupBy({
    by: ['date', 'slot'],
    where: {
      date: { gte: from, lt: to },
      status: { in: ['pending', 'confirmed'] },
    },
    _count: { _all: true },
  });
  const taken = new Map<string, number>();
  for (const c of counts) {
    taken.set(`${toDateOnly(c.date)}|${c.slot}`, c._count._all);
  }

  const todayStr = localToday();
  const nowMinutes = new Date().getHours() * 60 + new Date().getMinutes();

  const result: Array<{ date: string; slots: Array<{ slot: string; available: boolean }> }> = [];
  for (let i = 0; i < days; i++) {
    const day = new Date(from);
    day.setUTCDate(day.getUTCDate() + i);
    const dateStr = toDateOnly(day);
    if (!isOpenDay(dateStr)) continue;
    if (dateStr < todayStr) continue;

    const slots = BOOKING.slots.map((slot) => {
      const [h, m] = slot.split(':').map(Number);
      const isPastToday = dateStr === todayStr && h * 60 + m <= nowMinutes;
      const used = taken.get(`${dateStr}|${slot}`) ?? 0;
      return { slot, available: !isPastToday && used < capacity };
    });
    result.push({ date: dateStr, slots });
  }
  return result;
}

export async function createPublicAppointment(input: CreateAppointmentInput) {
  const service = await prisma.serviceCatalogItem.findFirst({
    where: { id: input.serviceId, active: true },
  });
  if (!service) throw new AppError('Unknown service', 400);

  if (!(BOOKING.slots as readonly string[]).includes(input.slot)) {
    throw new AppError('Invalid time slot', 400);
  }
  const todayStr = localToday();
  if (input.date < todayStr) throw new AppError('Date is in the past', 400);
  if (!isOpenDay(input.date)) throw new AppError('The shop is closed that day', 400);

  // Resolve a portal token to a customer (and validate the chosen vehicle)
  let customerId: string | null = null;
  let vehicleId: string | null = null;
  if (input.portalToken) {
    const customer = await prisma.customer.findFirst({
      where: { portalToken: input.portalToken, deletedAt: null },
      include: { vehicles: { where: { deletedAt: null }, select: { id: true } } },
    });
    if (!customer) throw new AppError('Invalid portal token', 400);
    customerId = customer.id;
    if (input.vehicleId) {
      if (!customer.vehicles.some((v) => v.id === input.vehicleId)) {
        throw new AppError('Vehicle does not belong to this customer', 400);
      }
      vehicleId = input.vehicleId;
    }
  }

  const shop = await prisma.shop.findFirst();
  const capacity = shop?.slotCapacity ?? 2;

  return prisma.$transaction(async (tx: TxClient) => {
    const clashing = await tx.appointment.count({
      where: {
        date: new Date(input.date),
        slot: input.slot,
        status: { in: ['pending', 'confirmed'] },
      },
    });
    if (clashing >= capacity) {
      throw new AppError('Slot no longer available', 409);
    }

    const no = await nextAppointmentNo(tx);
    return tx.appointment.create({
      data: {
        no,
        serviceId: service.id,
        customerId,
        vehicleId,
        name: input.name,
        phone: input.phone,
        email: input.email ?? null,
        vehicleYear: input.vehicleYear ?? null,
        vehicleMake: input.vehicleMake ?? null,
        vehicleModel: input.vehicleModel ?? null,
        notes: input.notes ?? null,
        date: new Date(input.date),
        slot: input.slot,
        durationMin: service.durationMin,
        status: 'pending',
      },
      include: { service: { select: { name: true, price: true, durationMin: true } } },
    });
  });
}

export async function transitionAppointment(
  id: string,
  newStatus: AppointmentStatus,
  userId: string,
  cancelReason?: string | null
) {
  return prisma.$transaction(async (tx: TxClient) => {
    const appt = await tx.appointment.findUniqueOrThrow({ where: { id } });
    const validNext = APPOINTMENT_TRANSITIONS[appt.status as AppointmentStatus] || [];
    if (!validNext.includes(newStatus)) {
      throw new AppError(`Cannot transition from ${appt.status} to ${newStatus}`, 400);
    }

    const result = await tx.appointment.updateMany({
      where: { id, status: appt.status },
      data: {
        status: newStatus,
        cancelReason: newStatus === 'cancelled' ? cancelReason ?? null : null,
      },
    });
    if (result.count === 0) {
      throw new AppError('Appointment changed concurrently, retry', 409);
    }

    await tx.auditLog.create({
      data: {
        userId,
        action: 'appointment_status',
        entityType: 'Appointment',
        entityId: id,
        details: { from: appt.status, to: newStatus },
      },
    });

    return tx.appointment.findUniqueOrThrow({
      where: { id },
      include: { service: true, customer: true, vehicle: true },
    });
  });
}

// Turn a confirmed (or still-pending) appointment into a work order.
// For guest bookings this creates the Customer and Vehicle records.
export async function convertAppointment(id: string, userId: string) {
  return prisma.$transaction(async (tx: TxClient) => {
    const appt = await tx.appointment.findUniqueOrThrow({
      where: { id },
      include: { service: true },
    });
    if (!['pending', 'confirmed'].includes(appt.status)) {
      throw new AppError(`Cannot convert a ${appt.status} appointment`, 400);
    }

    let customerId = appt.customerId;
    if (!customerId) {
      const customer = await tx.customer.create({
        data: {
          name: appt.name,
          phone: appt.phone,
          email: appt.email,
          portalToken: generatePortalToken(),
          notes: `Created from appointment ${appt.no}`,
        },
      });
      customerId = customer.id;
    }

    let vehicleId = appt.vehicleId;
    if (!vehicleId) {
      const vehicle = await tx.vehicle.create({
        data: {
          customerId,
          year: appt.vehicleYear ?? new Date().getFullYear(),
          make: appt.vehicleMake || 'Unknown',
          model: appt.vehicleModel || 'Unknown',
          plate: 'TBD',
        },
      });
      vehicleId = vehicle.id;
    }

    const shop = await tx.shop.findFirst();
    const no = await nextOrderNo(tx);
    const order = await tx.workOrder.create({
      data: {
        no,
        customerId,
        vehicleId,
        status: 'draft',
        notes: [appt.notes ? `Customer note: ${appt.notes}` : null, `Booked service: ${appt.service.name} (${appt.no})`]
          .filter(Boolean)
          .join('\n'),
        taxRate: shop?.taxRate ?? 0.0875,
        items: {
          create: [
            {
              kind: 'custom',
              description: appt.service.name,
              qty: 1,
              unitPrice: appt.service.price,
            },
          ],
        },
      },
      include: { customer: true, vehicle: true, items: true },
    });

    const updated = await tx.appointment.updateMany({
      where: { id, status: appt.status },
      data: { status: 'converted', convertedOrderId: order.id },
    });
    if (updated.count === 0) {
      throw new AppError('Appointment changed concurrently, retry', 409);
    }

    await tx.auditLog.create({
      data: {
        userId,
        action: 'convert_appointment',
        entityType: 'Appointment',
        entityId: id,
        details: { orderNo: order.no },
      },
    });

    return order;
  });
}
