import { randomUUID } from 'crypto';
import { promises as fs } from 'fs';
import path from 'path';
import { storageDataDirectory } from '../config/env';
import { readPersistentDataFile, writePersistentDataFile } from './local-store';

export const patientAttachmentCategories = [
  'LABORATORIO',
  'RADIOGRAFIA',
  'FOTOGRAFIA',
  'DOCUMENTO',
  'OTRO',
] as const;

export type PatientAttachmentCategory = (typeof patientAttachmentCategories)[number];

export type PatientAttachmentRecord = {
  id: string;
  patientId: string;
  category: PatientAttachmentCategory;
  title: string;
  notes: string | null;
  fileName: string;
  mimeType: string;
  fileSize: number;
  uploadedByName: string | null;
  uploadedAt: string;
  updatedAt: string;
};

type StoredPatientAttachment = PatientAttachmentRecord & {
  relativePath: string;
};

type CreatePatientAttachmentInput = {
  category: PatientAttachmentCategory;
  title: string;
  notes?: string | null;
  fileName: string;
  mimeType?: string | null;
  contentBase64: string;
  uploadedByName?: string | null;
};

const attachmentsFileName = 'patient-attachments.json';
const dataDirectory = storageDataDirectory;
const uploadsDirectory = path.join(dataDirectory, 'uploads', 'patients');

const imageMimeTypesByExtension: Record<string, string> = {
  '.jpg': 'image/jpeg',
  '.jpeg': 'image/jpeg',
  '.png': 'image/png',
  '.gif': 'image/gif',
  '.webp': 'image/webp',
  '.bmp': 'image/bmp',
  '.tif': 'image/tiff',
  '.tiff': 'image/tiff',
  '.pdf': 'application/pdf',
  '.doc': 'application/msword',
  '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  '.xls': 'application/vnd.ms-excel',
  '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  '.txt': 'text/plain',
};

const extensionByMimeType: Record<string, string> = {
  'image/jpeg': '.jpg',
  'image/png': '.png',
  'image/gif': '.gif',
  'image/webp': '.webp',
  'image/bmp': '.bmp',
  'image/tiff': '.tiff',
  'application/pdf': '.pdf',
  'application/msword': '.doc',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
  'application/vnd.ms-excel': '.xls',
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
  'text/plain': '.txt',
};

const defaultAttachments = () => [] as StoredPatientAttachment[];

export class InvalidPatientAttachmentError extends Error {
  constructor(message = 'No se pudo procesar el archivo adjunto') {
    super(message);
    this.name = 'InvalidPatientAttachmentError';
  }
}

export class PatientAttachmentNotFoundError extends Error {
  constructor() {
    super('Adjunto no encontrado');
    this.name = 'PatientAttachmentNotFoundError';
  }
}

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

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

const sanitizePathSegment = (value: string) => {
  const normalizedValue = value.trim().replace(/[^a-zA-Z0-9_-]+/g, '-');
  return normalizedValue || 'patient';
};

const sanitizeFileStem = (value: string) => {
  const normalizedValue = value.trim().replace(/[^a-zA-Z0-9._-]+/g, '-');
  const collapsedValue = normalizedValue.replace(/-+/g, '-').replace(/^\.+/, '').replace(/^[-.]+|[-.]+$/g, '');
  return collapsedValue || 'archivo';
};

const normalizeBase64Content = (value: string) => {
  const rawValue = value.includes(',') ? value.split(',').pop() ?? '' : value;
  return rawValue.replace(/\s+/g, '');
};

const getExtension = (fileName: string, mimeType?: string | null) => {
  const fileExtension = path.extname(fileName).toLowerCase();

  if (fileExtension) {
    return fileExtension;
  }

  if (mimeType && extensionByMimeType[mimeType]) {
    return extensionByMimeType[mimeType];
  }

  return '';
};

const getMimeType = (fileName: string, mimeType?: string | null) => {
  if (mimeType && mimeType.trim()) {
    return mimeType.trim();
  }

  const extension = path.extname(fileName).toLowerCase();
  return imageMimeTypesByExtension[extension] ?? 'application/octet-stream';
};

const readStoredAttachments = async () =>
  readPersistentDataFile<StoredPatientAttachment[]>(attachmentsFileName, defaultAttachments);

export const serializePatientAttachment = ({
  relativePath: _relativePath,
  ...attachment
}: StoredPatientAttachment): PatientAttachmentRecord => attachment;

export const listPatientAttachments = async (patientId: string) => {
  const attachments = await readStoredAttachments();

  return attachments
    .filter((attachment) => attachment.patientId === patientId)
    .sort((left, right) => new Date(right.uploadedAt).getTime() - new Date(left.uploadedAt).getTime());
};

export const createPatientAttachment = async (patientId: string, input: CreatePatientAttachmentInput) => {
  const normalizedTitle = input.title.trim();
  const normalizedFileName = path.basename(input.fileName.trim());
  const normalizedBase64 = normalizeBase64Content(input.contentBase64);

  if (!normalizedTitle) {
    throw new InvalidPatientAttachmentError('El titulo del adjunto es obligatorio');
  }

  if (!normalizedFileName) {
    throw new InvalidPatientAttachmentError('Debes seleccionar un archivo valido');
  }

  if (!normalizedBase64) {
    throw new InvalidPatientAttachmentError('No se encontro contenido para el archivo adjunto');
  }

  const fileBuffer = Buffer.from(normalizedBase64, 'base64');

  if (!fileBuffer.length) {
    throw new InvalidPatientAttachmentError('No se pudo leer el contenido del archivo adjunto');
  }

  const extension = getExtension(normalizedFileName, input.mimeType);
  const safeFileStem = sanitizeFileStem(path.basename(normalizedFileName, extension));
  const storedFileName = `${Date.now()}-${randomUUID()}${extension}`;
  const patientFolder = path.join(uploadsDirectory, sanitizePathSegment(patientId));
  const absoluteFilePath = path.join(patientFolder, storedFileName);
  const relativePath = path.relative(dataDirectory, absoluteFilePath).split(path.sep).join('/');
  const now = new Date().toISOString();
  const attachments = await readStoredAttachments();

  await fs.mkdir(patientFolder, { recursive: true });
  await fs.writeFile(absoluteFilePath, fileBuffer);

  const attachment: StoredPatientAttachment = {
    id: randomUUID(),
    patientId,
    category: input.category,
    title: normalizedTitle,
    notes: normalizeNullableText(input.notes),
    fileName: `${safeFileStem}${extension}`,
    mimeType: getMimeType(normalizedFileName, input.mimeType),
    fileSize: fileBuffer.length,
    uploadedByName: normalizeNullableText(input.uploadedByName),
    uploadedAt: now,
    updatedAt: now,
    relativePath,
  };

  attachments.unshift(attachment);
  await writePersistentDataFile(attachmentsFileName, attachments);

  return attachment;
};

export const findPatientAttachment = async (patientId: string, attachmentId: string) => {
  const attachments = await readStoredAttachments();

  return (
    attachments.find((attachment) => attachment.patientId === patientId && attachment.id === attachmentId) ?? null
  );
};

export const resolvePatientAttachmentPath = async (patientId: string, attachmentId: string) => {
  const attachment = await findPatientAttachment(patientId, attachmentId);

  if (!attachment) {
    throw new PatientAttachmentNotFoundError();
  }

  const absolutePath = path.resolve(dataDirectory, attachment.relativePath);
  await fs.access(absolutePath);

  return {
    attachment,
    absolutePath,
  };
};

export const deletePatientAttachment = async (patientId: string, attachmentId: string) => {
  const attachments = await readStoredAttachments();
  const attachmentIndex = attachments.findIndex(
    (attachment) => attachment.patientId === patientId && attachment.id === attachmentId,
  );

  if (attachmentIndex === -1) {
    throw new PatientAttachmentNotFoundError();
  }

  const attachment = attachments[attachmentIndex];

  if (!attachment) {
    throw new PatientAttachmentNotFoundError();
  }

  attachments.splice(attachmentIndex, 1);
  const absolutePath = path.resolve(dataDirectory, attachment.relativePath);

  await writePersistentDataFile(attachmentsFileName, attachments);

  try {
    await fs.unlink(absolutePath);
  } catch (error) {
    const fileError = error as NodeJS.ErrnoException;

    if (fileError.code !== 'ENOENT') {
      throw error;
    }
  }

  return attachment;
};
