import { EstadoCita, Prisma, Role } from '@prisma/client';
import type { Request, Response } from 'express';
import prisma from '../config/db';
import { buildDatabasePatientProfile, buildFallbackPatientProfile } from './patient.controller';
import { getPatientAuthContext } from '../middleware/patient-auth.middleware';
import { createFallbackAppointment, MissingFallbackDentistError, MissingFallbackPatientError } from '../services/appointment.store';
import {
  listNotificationFeed,
  markNotificationAsRead,
  recordAppointmentCommunications,
} from '../services/communication.store';
import {
  DoctorAvailabilityConflictError,
  DoctorScheduleNotFoundError,
  InvalidDoctorScheduleError,
  assertDoctorSlotAvailability,
  listAvailabilityDoctors,
  listDoctorAvailableSlots,
} from '../services/doctor-availability.service';
import { FallbackPatientNotFoundError, getFallbackPatientById, updateFallbackPatient } from '../services/patient.store';
import { findFallbackUserById, listFallbackUsers } from '../services/settings.store';

type SerializablePatientProfile = {
  id: string;
  dni: string;
  nombres: string;
  apellidos: string;
  fechaNacimiento?: Date | string | null;
  email: string | null;
  telefono: string | null;
  direccion?: string | null;
  fechaRegistro: Date | string;
  updatedAt: Date | string;
};

type PatientPortalProfileUpdateInput = {
  dni?: string;
  nombres?: string;
  apellidos?: string;
  fechaNacimiento?: string | null;
  email?: string | null;
  telefono?: string | null;
  direccion?: string | null;
};

type PatientPortalOverviewPayload = {
  patient: {
    id: string;
    dni: string;
    nombres: string;
    apellidos: string;
    nombreCompleto: string;
    fechaNacimiento: string | null;
    edad?: number | null;
    email: string | null;
    telefono: string | null;
    direccion: string | null;
    fechaRegistro: string;
    updatedAt: string;
  };
  appointments: Array<{
    id: string;
    fecha: string;
    motivo: string;
    estado: string;
    notas: string | null;
    dentistaNombre: string;
  }>;
  treatments: Array<{
    id: string;
    nombre: string;
    descripcion: string | null;
    costoBase: number;
    estado: string;
    piezaDental: string | null;
    fechaInicio: string | null;
    fechaFin: string | null;
    notasClinicas: string | null;
    activo: boolean;
    createdAt: string | null;
    updatedAt: string;
  }>;
  clinicalHistory: {
    totalTreatments: number;
    plannedCount: number;
    inProgressCount: number;
    completedCount: number;
    lastClinicalUpdate: string | null;
  };
  odontogram: {
    summary: {
      affectedTeethCount: number;
      cariesCount: number;
      restoredCount: number;
      extractedCount: number;
    };
    pieces: Array<{
      id: string;
      numero: number;
      estado: string;
      caras: string[];
      observacion: string | null;
      updatedAt: string;
    }>;
  };
  metrics: {
    upcomingAppointments: number;
    activeTreatmentCount: number;
    affectedTeethCount: number;
    completedTreatments: number;
  };
};

const assignableRoles = [Role.ADMIN, Role.DENTIST] as const;
const isAssignableRole = (role: Role) => role === Role.ADMIN || role === Role.DENTIST;

const normalizeNullableText = (value: unknown) => {
  if (typeof value !== 'string') {
    return null;
  }

  const trimmedValue = value.trim();
  return trimmedValue ? trimmedValue : null;
};

const normalizeDateInput = (value: unknown) => {
  if (typeof value !== 'string' || !value.trim()) {
    return null;
  }

  const directDate = new Date(value);

  if (!Number.isNaN(directDate.getTime())) {
    return directDate.toISOString();
  }

  const assumedDate = new Date(`${value}T00:00:00`);

  if (!Number.isNaN(assumedDate.getTime())) {
    return assumedDate.toISOString();
  }

  return null;
};

const buildAllowedPatientProfileUpdate = (body: Request['body']) => {
  const payload: PatientPortalProfileUpdateInput = {};

  if (typeof body.dni === 'string') payload.dni = body.dni.trim();
  if (typeof body.nombres === 'string') payload.nombres = body.nombres.trim();
  if (typeof body.apellidos === 'string') payload.apellidos = body.apellidos.trim();
  if (body.fechaNacimiento !== undefined) payload.fechaNacimiento = normalizeDateInput(body.fechaNacimiento);
  if (body.email !== undefined) payload.email = normalizeNullableText(body.email);
  if (body.telefono !== undefined) payload.telefono = normalizeNullableText(body.telefono);
  if (body.direccion !== undefined) payload.direccion = normalizeNullableText(body.direccion);

  return payload;
};

const serializePatientPortalProfile = (patient: SerializablePatientProfile) => ({
  id: patient.id,
  dni: patient.dni,
  nombres: patient.nombres,
  apellidos: patient.apellidos,
  nombreCompleto: `${patient.nombres} ${patient.apellidos}`.trim(),
  fechaNacimiento:
    patient.fechaNacimiento instanceof Date
      ? patient.fechaNacimiento.toISOString()
      : patient.fechaNacimiento ?? null,
  email: patient.email,
  telefono: patient.telefono,
  direccion: patient.direccion ?? null,
  fechaRegistro:
    patient.fechaRegistro instanceof Date ? patient.fechaRegistro.toISOString() : patient.fechaRegistro,
  updatedAt: patient.updatedAt instanceof Date ? patient.updatedAt.toISOString() : patient.updatedAt,
});

const serializePatientPortalOverview = (
  profile: Awaited<ReturnType<typeof buildDatabasePatientProfile>> | Awaited<ReturnType<typeof buildFallbackPatientProfile>>,
): PatientPortalOverviewPayload | null => {
  if (!profile) {
    return null;
  }

  return {
    patient: {
      id: profile.patient.id,
      dni: profile.patient.dni,
      nombres: profile.patient.nombres,
      apellidos: profile.patient.apellidos,
      nombreCompleto: profile.patient.nombreCompleto,
      fechaNacimiento: profile.patient.fechaNacimiento,
      edad: profile.patient.edad ?? null,
      email: profile.patient.email,
      telefono: profile.patient.telefono,
      direccion: profile.patient.direccion,
      fechaRegistro: profile.patient.fechaRegistro,
      updatedAt: profile.patient.updatedAt,
    },
    appointments: profile.appointments,
    treatments: profile.treatments,
    clinicalHistory: profile.clinicalHistory,
    odontogram: profile.odontogram,
    metrics: {
      upcomingAppointments: profile.metrics.upcomingAppointments,
      activeTreatmentCount: profile.metrics.activeTreatmentCount,
      affectedTeethCount: profile.odontogram.summary.affectedTeethCount,
      completedTreatments: profile.clinicalHistory.completedCount,
    },
  };
};

const normalizeDoctorId = (value: unknown) => (typeof value === 'string' && value.trim() ? value.trim() : null);

const parseAppointmentDate = (value: unknown) => {
  if (typeof value !== 'string' || !value.trim()) {
    return null;
  }

  const parsedDate = new Date(value);
  return Number.isNaN(parsedDate.getTime()) ? null : parsedDate;
};

const serializePatientPortalAppointment = (appointment: {
  id: string;
  fecha: Date | string;
  motivo: string;
  estado: string;
  notas?: string | null;
  dentista?: { id: string; name: string; email?: string | null } | null;
  dentistaId?: string;
  dentistaNombre?: string;
}) => ({
  id: appointment.id,
  fecha: appointment.fecha instanceof Date ? appointment.fecha.toISOString() : appointment.fecha,
  motivo: appointment.motivo,
  estado: appointment.estado,
  notas: appointment.notas ?? null,
  dentista: {
    id: appointment.dentista?.id ?? appointment.dentistaId ?? '',
    nombre: appointment.dentista?.name ?? appointment.dentistaNombre ?? 'Agenda del Sistema',
  },
});

const resolveFallbackAssignableDentist = async (doctorUserId: string | null) => {
  if (doctorUserId) {
    const fallbackUser = await findFallbackUserById(doctorUserId);

    if (fallbackUser && fallbackUser.active && isAssignableRole(fallbackUser.role)) {
      return fallbackUser;
    }

    return null;
  }

  const users = await listFallbackUsers();
  return users.find((user) => user.active && isAssignableRole(user.role)) ?? null;
};

const emitPatientPortalAppointmentCommunication = async (input: {
  sourceMode: 'database' | 'local-fallback';
  appointmentId: string;
  startsAt: string;
  motivo: string;
  patient: {
    id: string;
    nombres: string;
    apellidos: string;
    email: string | null;
    telefono: string | null;
  };
  dentist: {
    id: string;
    nombre: string;
    email: string | null;
  };
}) => {
  try {
    await recordAppointmentCommunications({
      eventType: 'APPOINTMENT_CREATED',
      appointmentId: input.appointmentId,
      startsAt: input.startsAt,
      patient: {
        id: input.patient.id,
        name: `${input.patient.nombres} ${input.patient.apellidos}`.trim(),
        email: input.patient.email,
        phone: input.patient.telefono,
      },
      dentist: {
        id: input.dentist.id,
        name: input.dentist.nombre,
        email: input.dentist.email,
      },
      motivo: input.motivo,
      status: EstadoCita.PROGRAMADA,
      sourceMode: input.sourceMode,
      reason: null,
    });
  } catch (communicationError) {
    console.error('Error emitting patient portal appointment communications:', communicationError);
  }
};

const handlePortalAvailabilityError = (error: unknown, res: Response) => {
  if (error instanceof DoctorAvailabilityConflictError) {
    return res.status(409).json({ error: error.message });
  }

  if (error instanceof InvalidDoctorScheduleError) {
    return res.status(400).json({ error: error.message });
  }

  if (error instanceof DoctorScheduleNotFoundError) {
    return res.status(404).json({ error: error.message });
  }

  return null;
};

export const getPatientPortalProfile = async (req: Request, res: Response) => {
  const patientAuth = getPatientAuthContext(req);

  if (!patientAuth) {
    return res.status(401).json({ error: 'Debes iniciar sesion como paciente.' });
  }

  if (patientAuth.sourceMode === 'database') {
    const patient = await prisma.paciente.findUnique({
      where: { id: patientAuth.patientId },
    });

    if (!patient) {
      return res.status(404).json({ error: 'Paciente no encontrado.' });
    }

    res.setHeader('X-Data-Source', 'database');
    return res.json({
      patient: serializePatientPortalProfile(patient),
    });
  }

  const fallbackPatient = await getFallbackPatientById(patientAuth.patientId);

  if (!fallbackPatient) {
    return res.status(404).json({ error: 'Paciente no encontrado.' });
  }

  res.setHeader('X-Data-Source', 'local-fallback');
  return res.json({
    patient: serializePatientPortalProfile(fallbackPatient),
  });
};

export const updatePatientPortalProfile = async (req: Request, res: Response) => {
  const patientAuth = getPatientAuthContext(req);

  if (!patientAuth) {
    return res.status(401).json({ error: 'Debes iniciar sesion como paciente.' });
  }

  const payload = buildAllowedPatientProfileUpdate(req.body);

  if (payload.dni !== undefined && !payload.dni) {
    return res.status(400).json({ error: 'El documento no puede quedar vacio.' });
  }

  if (payload.nombres !== undefined && !payload.nombres) {
    return res.status(400).json({ error: 'Los nombres no pueden quedar vacios.' });
  }

  if (payload.apellidos !== undefined && !payload.apellidos) {
    return res.status(400).json({ error: 'Los apellidos no pueden quedar vacios.' });
  }

  try {
    const updatedPatient = await prisma.paciente.update({
      where: { id: patientAuth.patientId },
      data: {
        ...(payload.dni !== undefined ? { dni: payload.dni } : {}),
        ...(payload.nombres !== undefined ? { nombres: payload.nombres } : {}),
        ...(payload.apellidos !== undefined ? { apellidos: payload.apellidos } : {}),
        ...(payload.fechaNacimiento !== undefined
          ? { fechaNacimiento: payload.fechaNacimiento ? new Date(payload.fechaNacimiento) : null }
          : {}),
        ...(payload.email !== undefined ? { email: payload.email } : {}),
        ...(payload.telefono !== undefined ? { telefono: payload.telefono } : {}),
        ...(payload.direccion !== undefined ? { direccion: payload.direccion } : {}),
      },
    });

    res.setHeader('X-Data-Source', 'database');
    return res.json({
      patient: serializePatientPortalProfile(updatedPatient),
    });
  } catch (error) {
    if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
      return res.status(409).json({ error: 'Ya existe un paciente con ese documento.' });
    }

    console.warn('Database unavailable while updating patient portal profile, using local fallback store.', error);
  }

  try {
    const updatedPatient = await updateFallbackPatient(patientAuth.patientId, payload);
    res.setHeader('X-Data-Source', 'local-fallback');
    return res.json({
      patient: serializePatientPortalProfile(updatedPatient),
    });
  } catch (error) {
    if (error instanceof FallbackPatientNotFoundError) {
      return res.status(404).json({ error: 'Paciente no encontrado.' });
    }

    return res.status(500).json({ error: 'Error interno del servidor.' });
  }
};

export const getPatientPortalOverview = async (req: Request, res: Response) => {
  const patientAuth = getPatientAuthContext(req);

  if (!patientAuth) {
    return res.status(401).json({ error: 'Debes iniciar sesion como paciente.' });
  }

  try {
    const profile = await buildDatabasePatientProfile(patientAuth.patientId);

    if (!profile) {
      return res.status(404).json({ error: 'Paciente no encontrado.' });
    }

    res.setHeader('X-Data-Source', 'database');
    return res.json({
      overview: serializePatientPortalOverview(profile),
    });
  } catch (error) {
    console.warn('Database unavailable while loading patient portal overview, using local fallback store.', error);
  }

  try {
    const fallbackProfile = await buildFallbackPatientProfile(patientAuth.patientId);

    if (!fallbackProfile) {
      return res.status(404).json({ error: 'Paciente no encontrado.' });
    }

    res.setHeader('X-Data-Source', 'local-fallback');
    return res.json({
      overview: serializePatientPortalOverview(fallbackProfile),
    });
  } catch (error) {
    console.error('Error loading patient portal overview:', error);
    return res.status(500).json({ error: 'Error interno del servidor.' });
  }
};

export const getPatientPortalDoctors = async (_req: Request, res: Response) => {
  try {
    const result = await listAvailabilityDoctors();
    res.setHeader('X-Data-Source', result.sourceMode);
    return res.json({ doctors: result.doctors });
  } catch (error) {
    return res.status(500).json({ error: 'Error interno del servidor.' });
  }
};

export const getPatientPortalDoctorSlots = async (req: Request, res: Response) => {
  const doctorUserId = normalizeDoctorId(req.params.doctorUserId);
  const dateKey = typeof req.query.date === 'string' ? req.query.date : '';

  if (!doctorUserId) {
    return res.status(400).json({ error: 'Odontologo invalido.' });
  }

  if (!dateKey) {
    return res.status(400).json({ error: 'Debes indicar la fecha a consultar.' });
  }

  try {
    const result = await listDoctorAvailableSlots(doctorUserId, dateKey);
    res.setHeader('X-Data-Source', result.sourceMode);
    return res.json({
      date: result.dateKey,
      effectiveSchedule: result.effectiveSchedule,
      slots: result.slots,
    });
  } catch (error) {
    const availabilityResponse = handlePortalAvailabilityError(error, res);

    if (availabilityResponse) {
      return availabilityResponse;
    }

    console.error('Error loading patient portal availability slots:', error);
    return res.status(500).json({ error: 'Error interno del servidor.' });
  }
};

export const createPatientPortalAppointment = async (req: Request, res: Response) => {
  const patientAuth = getPatientAuthContext(req);

  if (!patientAuth) {
    return res.status(401).json({ error: 'Debes iniciar sesion como paciente.' });
  }

  const doctorUserId = normalizeDoctorId(req.body.doctorUserId ?? req.body.dentistaId);
  const fecha = parseAppointmentDate(req.body.fecha);
  const motivo = typeof req.body.motivo === 'string' ? req.body.motivo.trim() : '';

  if (!doctorUserId) {
    return res.status(400).json({ error: 'Debes seleccionar un odontologo.' });
  }

  if (!fecha) {
    return res.status(400).json({ error: 'Debes seleccionar una fecha valida para la cita.' });
  }

  if (!motivo) {
    return res.status(400).json({ error: 'El motivo de la cita es obligatorio.' });
  }

  try {
    const patient = await prisma.paciente.findUnique({
      where: { id: patientAuth.patientId },
    });

    if (!patient) {
      return res.status(404).json({ error: 'Paciente no encontrado.' });
    }

    const dentist = await prisma.user.findFirst({
      where: {
        id: doctorUserId,
        active: true,
        role: {
          in: [...assignableRoles],
        },
      },
    });

    if (!dentist) {
      return res.status(404).json({ error: 'Odontologo no encontrado.' });
    }

    await assertDoctorSlotAvailability(dentist.id, fecha);

    const appointment = await prisma.cita.create({
      data: {
        fecha,
        motivo,
        estado: EstadoCita.PROGRAMADA,
        pacienteId: patientAuth.patientId,
        dentistaId: dentist.id,
      },
      include: {
        paciente: true,
        dentista: true,
      },
    });

    await emitPatientPortalAppointmentCommunication({
      sourceMode: 'database',
      appointmentId: appointment.id,
      startsAt: appointment.fecha.toISOString(),
      motivo: appointment.motivo,
      patient: {
        id: appointment.paciente.id,
        nombres: appointment.paciente.nombres,
        apellidos: appointment.paciente.apellidos,
        email: appointment.paciente.email,
        telefono: appointment.paciente.telefono,
      },
      dentist: {
        id: appointment.dentista.id,
        nombre: appointment.dentista.name,
        email: appointment.dentista.email,
      },
    });

    res.setHeader('X-Data-Source', 'database');
    return res.status(201).json({
      appointment: serializePatientPortalAppointment(appointment),
      message: 'Cita reservada correctamente.',
    });
  } catch (error) {
    const availabilityResponse = handlePortalAvailabilityError(error, res);

    if (availabilityResponse) {
      return availabilityResponse;
    }

    console.warn('Database unavailable while creating patient portal appointment, using local fallback store.', error);
  }

  try {
    const fallbackPatient = await getFallbackPatientById(patientAuth.patientId);

    if (!fallbackPatient) {
      return res.status(404).json({ error: 'Paciente no encontrado.' });
    }

    const fallbackDentist = await resolveFallbackAssignableDentist(doctorUserId);

    if (!fallbackDentist) {
      return res.status(404).json({ error: 'Odontologo no encontrado.' });
    }

    await assertDoctorSlotAvailability(fallbackDentist.id, fecha);

    const appointment = await createFallbackAppointment({
      pacienteId: patientAuth.patientId,
      dentistaId: fallbackDentist.id,
      fecha: fecha.toISOString(),
      motivo,
      estado: EstadoCita.PROGRAMADA,
      notas: null,
    });

    await emitPatientPortalAppointmentCommunication({
      sourceMode: 'local-fallback',
      appointmentId: appointment.id,
      startsAt: appointment.fecha,
      motivo: appointment.motivo,
      patient: {
        id: appointment.pacienteId,
        nombres: appointment.pacienteNombres,
        apellidos: appointment.pacienteApellidos,
        email: appointment.pacienteEmail,
        telefono: appointment.pacienteTelefono,
      },
      dentist: {
        id: appointment.dentistaId,
        nombre: appointment.dentistaNombre,
        email: fallbackDentist.email,
      },
    });

    res.setHeader('X-Data-Source', 'local-fallback');
    return res.status(201).json({
      appointment: serializePatientPortalAppointment({
        id: appointment.id,
        fecha: appointment.fecha,
        motivo: appointment.motivo,
        estado: appointment.estado,
        notas: appointment.notas,
        dentistaId: appointment.dentistaId,
        dentistaNombre: appointment.dentistaNombre,
      }),
      message: 'Cita reservada correctamente.',
    });
  } catch (error) {
    const availabilityResponse = handlePortalAvailabilityError(error, res);

    if (availabilityResponse) {
      return availabilityResponse;
    }

    if (error instanceof MissingFallbackPatientError || error instanceof MissingFallbackDentistError) {
      return res.status(404).json({ error: error.message });
    }

    console.error('Error creating patient portal appointment:', error);
    return res.status(500).json({ error: 'Error interno del servidor.' });
  }
};

export const getPatientPortalNotifications = async (req: Request, res: Response) => {
  const patientAuth = getPatientAuthContext(req);

  if (!patientAuth) {
    return res.status(401).json({ error: 'Debes iniciar sesion como paciente.' });
  }

  try {
    const notifications = await listNotificationFeed(patientAuth.accountId, {
      includeBroadcast: false,
    });
    const unreadCount = notifications.filter((notification) => !notification.readAt).length;

    res.setHeader('X-Data-Source', 'local-fallback');
    return res.json({
      notifications,
      unreadCount,
    });
  } catch (error) {
    console.error('Error loading patient portal notifications:', error);
    return res.status(500).json({ error: 'Error interno del servidor.' });
  }
};

export const readPatientPortalNotification = async (req: Request, res: Response) => {
  const patientAuth = getPatientAuthContext(req);

  if (!patientAuth) {
    return res.status(401).json({ error: 'Debes iniciar sesion como paciente.' });
  }

  const notificationId = normalizeNullableText(req.params.notificationId);

  if (!notificationId) {
    return res.status(400).json({ error: 'La notificacion indicada no es valida.' });
  }

  try {
    await markNotificationAsRead(notificationId, patientAuth.accountId, {
      allowBroadcast: false,
    });

    return res.json({ ok: true });
  } catch (error) {
    console.error('Error marking patient portal notification as read:', error);
    return res.status(500).json({ error: 'Error interno del servidor.' });
  }
};
