import { EstadoCita, Role } from '@prisma/client';
import { randomUUID } from 'crypto';
import { promises as fs } from 'fs';
import path from 'path';
import { storageDataDirectory } from '../config/env';
import { listFallbackPatients } from './patient.store';
import { findFallbackUserById, listFallbackUsers } from './settings.store';

type StoredAppointment = {
  id: string;
  fecha: string;
  motivo: string;
  estado: EstadoCita;
  notas: string | null;
  pacienteId: string;
  pacienteNombres: string;
  pacienteApellidos: string;
  pacienteEmail: string | null;
  pacienteTelefono: string | null;
  dentistaId: string;
  dentistaNombre: string;
  createdAt: string;
  updatedAt: string;
};

type CreateAppointmentInput = {
  pacienteId: string;
  fecha: string;
  motivo: string;
  estado?: EstadoCita;
  notas?: string | null;
  dentistaId?: string | null;
};

type UpdateAppointmentInput = {
  fecha?: string;
  motivo?: string;
  estado?: EstadoCita;
  notas?: string | null;
  dentistaId?: string;
};

export class MissingFallbackPatientError extends Error {
  constructor() {
    super('No se encontro el paciente en el almacenamiento local.');
  }
}

export class MissingFallbackDentistError extends Error {
  constructor() {
    super('No se encontro el odontologo en el almacenamiento local.');
  }
}

export class FallbackAppointmentNotFoundError extends Error {
  constructor() {
    super('Cita no encontrada');
  }
}

const dataDirectory = storageDataDirectory;
const dataFilePath = path.join(dataDirectory, 'appointments.json');

const isFallbackDentist = (role: Role) => role === Role.DENTIST || role === Role.ADMIN;

const resolveFallbackDentist = async (dentistaId?: string | null) => {
  if (dentistaId) {
    const selectedUser = await findFallbackUserById(dentistaId);

    if (selectedUser && selectedUser.active && isFallbackDentist(selectedUser.role)) {
      return selectedUser;
    }

    throw new MissingFallbackDentistError();
  }

  const users = await listFallbackUsers();
  const fallbackDentist = users.find((user) => user.active && isFallbackDentist(user.role));

  if (!fallbackDentist) {
    throw new MissingFallbackDentistError();
  }

  return fallbackDentist;
};

const buildSeedAppointments = async () => {
  const patients = await listFallbackPatients();
  const users = await listFallbackUsers();
  const fallbackDentist = users.find((user) => user.active && isFallbackDentist(user.role));

  if (patients.length === 0 || !fallbackDentist) {
    return [] as StoredAppointment[];
  }

  const today = new Date();
  today.setHours(0, 0, 0, 0);

  const templates = [
    { hour: 9, minute: 0, motivo: 'Evaluacion general', estado: EstadoCita.CONFIRMADA },
    { hour: 10, minute: 30, motivo: 'Limpieza dental', estado: EstadoCita.PROGRAMADA },
    { hour: 12, minute: 0, motivo: 'Control de ortodoncia', estado: EstadoCita.EN_SALA },
  ] as const;

  return templates.map((template, index) => {
    const patient = patients[index % patients.length]!;
    const appointmentDate = new Date(today);
    appointmentDate.setHours(template.hour, template.minute, 0, 0);
    const isoDate = appointmentDate.toISOString();

    return {
      id: randomUUID(),
      fecha: isoDate,
      motivo: template.motivo,
      estado: template.estado,
      notas: null,
      pacienteId: patient.id,
      pacienteNombres: patient.nombres,
      pacienteApellidos: patient.apellidos,
      pacienteEmail: patient.email,
      pacienteTelefono: patient.telefono,
      dentistaId: fallbackDentist.id,
      dentistaNombre: fallbackDentist.name,
      createdAt: isoDate,
      updatedAt: isoDate,
    };
  });
};

const ensureStore = async () => {
  try {
    await fs.access(dataFilePath);
  } catch {
    const seedAppointments = await buildSeedAppointments();
    await fs.mkdir(dataDirectory, { recursive: true });
    await fs.writeFile(dataFilePath, JSON.stringify(seedAppointments, null, 2));
  }
};

const readAppointments = async () => {
  await ensureStore();

  const fileContent = await fs.readFile(dataFilePath, 'utf-8');
  const appointments = JSON.parse(fileContent) as Array<StoredAppointment & { dentistaId?: string }>;
  const users = await listFallbackUsers();
  const fallbackDentist = users.find((user) => user.active && isFallbackDentist(user.role));
  let hasChanges = false;

  const normalizedAppointments = appointments.map((appointment) => {
    if (appointment.dentistaId) {
      return appointment as StoredAppointment;
    }

    hasChanges = true;

    return {
      ...appointment,
      dentistaId: fallbackDentist?.id ?? 'local-fallback',
      dentistaNombre: appointment.dentistaNombre || fallbackDentist?.name || 'Agenda del Sistema',
    };
  });

  if (hasChanges) {
    await writeAppointments(normalizedAppointments);
  }

  return normalizedAppointments;
};

const writeAppointments = async (appointments: StoredAppointment[]) => {
  await fs.mkdir(dataDirectory, { recursive: true });
  await fs.writeFile(dataFilePath, JSON.stringify(appointments, null, 2));
};

const getDayBounds = (selectedDate: Date) => {
  const dayStart = new Date(selectedDate);
  dayStart.setHours(0, 0, 0, 0);

  const dayEnd = new Date(selectedDate);
  dayEnd.setHours(23, 59, 59, 999);

  return { dayStart, dayEnd };
};

export const listFallbackAppointments = async (selectedDate?: Date) => {
  const appointments = await readAppointments();

  const filteredAppointments = selectedDate
    ? appointments.filter((appointment) => {
        const appointmentDate = new Date(appointment.fecha);
        const { dayStart, dayEnd } = getDayBounds(selectedDate);

        return appointmentDate >= dayStart && appointmentDate <= dayEnd;
      })
    : appointments;

  return filteredAppointments.sort(
    (left, right) => new Date(left.fecha).getTime() - new Date(right.fecha).getTime(),
  );
};

export const createFallbackAppointment = async (input: CreateAppointmentInput) => {
  const patients = await listFallbackPatients();
  const patient = patients.find((item) => item.id === input.pacienteId);

  if (!patient) {
    throw new MissingFallbackPatientError();
  }

  const dentist = await resolveFallbackDentist(input.dentistaId);

  const appointments = await readAppointments();
  const now = new Date().toISOString();

  const appointment: StoredAppointment = {
    id: randomUUID(),
    fecha: input.fecha,
    motivo: input.motivo,
    estado: input.estado ?? EstadoCita.PROGRAMADA,
    notas: input.notas ?? null,
    pacienteId: patient.id,
    pacienteNombres: patient.nombres,
    pacienteApellidos: patient.apellidos,
    pacienteEmail: patient.email,
    pacienteTelefono: patient.telefono,
    dentistaId: dentist.id,
    dentistaNombre: dentist.name,
    createdAt: now,
    updatedAt: now,
  };

  appointments.push(appointment);
  await writeAppointments(appointments);

  return appointment;
};

export const updateFallbackAppointment = async (appointmentId: string, input: UpdateAppointmentInput) => {
  const appointments = await readAppointments();
  const appointmentIndex = appointments.findIndex((appointment) => appointment.id === appointmentId);

  if (appointmentIndex === -1) {
    throw new FallbackAppointmentNotFoundError();
  }

  const currentAppointment = appointments[appointmentIndex]!;
  const nextDentist = input.dentistaId
    ? await resolveFallbackDentist(input.dentistaId)
    : {
        id: currentAppointment.dentistaId,
        name: currentAppointment.dentistaNombre,
      };
  const updatedAppointment: StoredAppointment = {
    id: currentAppointment.id,
    fecha: input.fecha ?? currentAppointment.fecha,
    motivo: input.motivo ?? currentAppointment.motivo,
    estado: input.estado ?? currentAppointment.estado,
    notas: input.notas !== undefined ? input.notas : currentAppointment.notas,
    pacienteId: currentAppointment.pacienteId,
    pacienteNombres: currentAppointment.pacienteNombres,
    pacienteApellidos: currentAppointment.pacienteApellidos,
    pacienteEmail: currentAppointment.pacienteEmail,
    pacienteTelefono: currentAppointment.pacienteTelefono,
    dentistaId: nextDentist.id,
    dentistaNombre: nextDentist.name,
    createdAt: currentAppointment.createdAt,
    updatedAt: new Date().toISOString(),
  };

  appointments[appointmentIndex] = updatedAppointment;
  await writeAppointments(appointments);

  return updatedAppointment;
};
