import {
  BadRequestException,
  ConflictException,
  ForbiddenException,
  HttpException,
  HttpStatus,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { createHash } from 'crypto';
import { Prisma } from '@prisma/client';
import { envs } from '../../config';
import { PrismaService } from '../../prisma/prisma.service';
import {
  AnularDocumentoDto,
  LegajoVencimientoEstado,
  QueryLegajoEmpleadoDto,
  ReemplazarDocumentoDto,
  SubirDocumentoDto,
} from '../dto/legajos.dto';
import { LegajosMimeValidator } from './legajos-mime.validator';
import { LegajosStorageService } from './legajos-storage.service';
import { ParametrosService } from './parametros.service';

interface UploadFileLike {
  buffer: Buffer;
  originalname: string;
  mimetype?: string;
  size: number;
}

/**
 * Lógica de negocio del repositorio documental por funcionario (M24).
 *
 * Responsabilidades:
 *  - Subir documento (multipart): validar tipo, MIME, tamaño, hash, almacenamiento, versionado.
 *  - Reemplazar: marca el anterior como REEMPLAZADO y vincula la cadena de versiones.
 *  - Anular: motivo obligatorio, sólo desde ACTIVO.
 *  - Generar URLs firmadas de descarga / preview.
 *  - Consultar legajo de un funcionario (ACTIVOS) e historial (REEMPLAZADO / ANULADO).
 *
 * Las reglas vienen del PDF Novasis_ERP_RRHH_Legajos_v1 secciones 4.2, 4.6 y 8.
 */
@Injectable()
export class LegajosDocumentosService {
  private readonly logger = new Logger(LegajosDocumentosService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly storage: LegajosStorageService,
    private readonly mimeValidator: LegajosMimeValidator,
    private readonly parametros: ParametrosService,
  ) {}

  // ──────────────────────────────────────────────────────────────────────
  // Consultas
  // ──────────────────────────────────────────────────────────────────────

  async listarLegajoEmpleado(
    empresaId: string,
    empleadoId: string,
    query: QueryLegajoEmpleadoDto,
  ) {
    await this.validarEmpleado(empresaId, empleadoId);

    const where: Prisma.rrhh_legajo_documentosWhereInput = {
      empresa_id: empresaId,
      empleado_id: empleadoId,
      estado: 'ACTIVO',
    };
    if (query.estado_vencimiento) {
      where.estado_vencimiento = query.estado_vencimiento as any;
    }
    if (query.tipo_documento_id) {
      where.tipo_documento_id = query.tipo_documento_id;
    }

    const docs = await this.prisma.rrhh_legajo_documentos.findMany({
      where,
      orderBy: [{ tipo_documento_id: 'asc' }, { subido_en: 'desc' }],
      include: {
        tipo_documento: {
          select: { id: true, codigo: true, nombre: true, admite_vencimiento: true, es_obligatorio: true },
        },
      },
    });

    return docs.map((d) => this.serializar(d));
  }

  async obtener(documentoId: string, empresaId: string) {
    const doc = await this.prisma.rrhh_legajo_documentos.findFirst({
      where: { id: documentoId, empresa_id: empresaId },
      include: { tipo_documento: { select: { codigo: true, nombre: true } } },
    });
    if (!doc) throw new NotFoundException('Documento no encontrado');
    return this.serializar(doc);
  }

  async obtenerHistorialEmpleado(empresaId: string, empleadoId: string) {
    await this.validarEmpleado(empresaId, empleadoId);
    const docs = await this.prisma.rrhh_legajo_documentos.findMany({
      where: {
        empresa_id: empresaId,
        empleado_id: empleadoId,
        estado: { in: ['REEMPLAZADO', 'ANULADO'] },
      },
      orderBy: { subido_en: 'desc' },
      include: { tipo_documento: { select: { codigo: true, nombre: true } } },
    });
    return docs.map((d) => this.serializar(d));
  }

  // ──────────────────────────────────────────────────────────────────────
  // Generación de URLs firmadas
  // ──────────────────────────────────────────────────────────────────────

  async presignedDownload(documentoId: string, empresaId: string) {
    const doc = await this.cargarSiNoAnulado(documentoId, empresaId);
    const url = await this.storage.presignedDownloadUrl(doc.nombre_storage, doc.nombre_original);
    return { url, expira_en_segundos: this.expiraEnSeg() };
  }

  async presignedPreview(documentoId: string, empresaId: string) {
    const doc = await this.cargarSiNoAnulado(documentoId, empresaId);
    const url = await this.storage.presignedPreviewUrl(doc.nombre_storage, doc.mime_type);
    return { url, mime_type: doc.mime_type, expira_en_segundos: this.expiraEnSeg() };
  }

  // ──────────────────────────────────────────────────────────────────────
  // Carga / reemplazo
  // ──────────────────────────────────────────────────────────────────────

  async subir(
    empresaId: string,
    empleadoId: string,
    dto: SubirDocumentoDto,
    archivo: UploadFileLike | undefined,
    userId: string,
  ) {
    if (!archivo || !archivo.buffer || archivo.buffer.length === 0) {
      throw new BadRequestException('Debe adjuntar el archivo en el campo "archivo"');
    }

    const empleado = await this.validarEmpleado(empresaId, empleadoId);
    const tipo = await this.validarTipo(empresaId, dto.tipo_documento_id);

    await this.validarTamanio(empresaId, archivo.size);
    const mimeHabilitados = await this.cargarMimeHabilitados(empresaId);
    const mimeOk = await this.mimeValidator.validate(
      archivo.buffer,
      archivo.mimetype,
      archivo.originalname,
      mimeHabilitados,
    );

    const fechaDoc = dto.fecha_documento ? new Date(dto.fecha_documento) : null;
    const fechaVenc = this.parseFechaVencimiento(dto.fecha_vencimiento, tipo);
    const estadoVenc = this.calcularEstadoVencimiento(fechaVenc, tipo.dias_alerta_vencimiento);

    const hash = this.calcularHashSHA256(archivo.buffer);
    const duplicado = await this.detectarDuplicado(empresaId, empleadoId, tipo.id, hash);

    // ¿Existe un documento previo ACTIVO del mismo tipo? -> versionado o conflicto
    let documentoPrevio: { id: string; version: number } | null = null;
    if (!tipo.permite_multiples) {
      const previo = await this.prisma.rrhh_legajo_documentos.findFirst({
        where: {
          empresa_id: empresaId,
          empleado_id: empleadoId,
          tipo_documento_id: tipo.id,
          estado: 'ACTIVO',
        },
        select: { id: true, version: true },
      });
      if (previo) {
        documentoPrevio = previo;
      }
    }

    // Crear registro PRIMERO en BD con el ID generado, calcular la key con ese ID,
    // subir al storage y actualizar la fila con `nombre_storage`. Lo hago todo dentro
    // de una transacción Prisma — si el upload falla, ROLLBACK; no queda doc huérfano.
    return this.prisma.$transaction(async (tx) => {
      // 1) Reservar id + version
      const nuevaVersion = documentoPrevio ? documentoPrevio.version + 1 : 1;

      // Pre-generamos el UUID llamando a la BD (la convención es gen_random_uuid / uuid_generate_v4 ya seteado por default)
      // Hacemos el insert con storage_key calculado a partir del id devuelto.
      const created = await tx.rrhh_legajo_documentos.create({
        data: {
          empresa_id: empresaId,
          empleado_id: empleadoId,
          tipo_documento_id: tipo.id,
          nombre_original: archivo.originalname,
          nombre_storage: 'PENDING', // se actualiza tras el upload
          mime_type: mimeOk.mime,
          extension: mimeOk.extension,
          tamanio_bytes: BigInt(archivo.buffer.length),
          hash_sha256: hash,
          fecha_documento: fechaDoc,
          fecha_vencimiento: fechaVenc,
          estado_vencimiento: estadoVenc as any,
          descripcion: dto.descripcion ?? null,
          version: nuevaVersion,
          documento_previo_id: documentoPrevio?.id ?? null,
          estado: 'ACTIVO',
          subido_por: userId,
          subido_en: new Date(),
        },
      });

      // 2) Subir al storage con la ruta canónica
      const storageKey = this.storage.buildStorageKey({
        empresaId,
        empleadoId,
        tipoCodigo: tipo.codigo,
        documentoId: created.id,
        extension: mimeOk.extension,
      });

      try {
        await this.storage.upload({
          empresaId,
          empleadoId,
          tipoCodigo: tipo.codigo,
          documentoId: created.id,
          extension: mimeOk.extension,
          mimeType: mimeOk.mime,
          buffer: archivo.buffer,
        });
      } catch (err) {
        // El throw propaga el rollback de la transacción.
        this.logger.error(`Upload falló para ${created.id}: ${err instanceof Error ? err.message : err}`);
        throw err;
      }

      // 3) Actualizar nombre_storage con la ruta real
      const updated = await tx.rrhh_legajo_documentos.update({
        where: { id: created.id },
        data: { nombre_storage: storageKey },
      });

      // 4) Marcar previo como REEMPLAZADO
      if (documentoPrevio) {
        await tx.rrhh_legajo_documentos.update({
          where: { id: documentoPrevio.id },
          data: { estado: 'REEMPLAZADO' },
        });
      }

      // 5) Generar URL de preview para devolver al cliente
      const previewUrl =
        mimeOk.mime.startsWith('image/') || mimeOk.mime === 'application/pdf'
          ? await this.storage.presignedPreviewUrl(storageKey, mimeOk.mime)
          : null;

      return {
        documento: this.serializar({
          ...updated,
          tipo_documento: {
            id: tipo.id,
            codigo: tipo.codigo,
            nombre: tipo.nombre,
            admite_vencimiento: tipo.admite_vencimiento,
            es_obligatorio: tipo.es_obligatorio,
          },
        } as any),
        empleado_id: empleado.id,
        duplicado_detectado: duplicado,
        preview_url: previewUrl,
      };
    });
  }

  async reemplazar(
    documentoId: string,
    empresaId: string,
    dto: ReemplazarDocumentoDto,
    archivo: UploadFileLike | undefined,
    userId: string,
  ) {
    const actual = await this.prisma.rrhh_legajo_documentos.findFirst({
      where: { id: documentoId, empresa_id: empresaId },
      include: { tipo_documento: true },
    });
    if (!actual) throw new NotFoundException('Documento no encontrado');
    if (actual.estado === 'ANULADO') {
      throw new ConflictException(
        'No se puede reemplazar un documento ANULADO. Realice una carga nueva del mismo tipo.',
      );
    }
    if (actual.estado === 'REEMPLAZADO') {
      throw new ConflictException(
        'Este documento ya fue reemplazado anteriormente. Reemplace la versión vigente.',
      );
    }

    return this.subir(
      empresaId,
      actual.empleado_id,
      {
        tipo_documento_id: actual.tipo_documento_id,
        fecha_documento: dto.fecha_documento,
        fecha_vencimiento: dto.fecha_vencimiento,
        descripcion: dto.descripcion,
      },
      archivo,
      userId,
    );
  }

  // ──────────────────────────────────────────────────────────────────────
  // Anulación
  // ──────────────────────────────────────────────────────────────────────

  async anular(
    documentoId: string,
    empresaId: string,
    dto: AnularDocumentoDto,
    userId: string,
  ) {
    const doc = await this.prisma.rrhh_legajo_documentos.findFirst({
      where: { id: documentoId, empresa_id: empresaId },
    });
    if (!doc) throw new NotFoundException('Documento no encontrado');

    if (doc.estado === 'ANULADO') {
      throw new ConflictException('El documento ya está anulado.');
    }
    if (doc.estado === 'REEMPLAZADO') {
      throw new ForbiddenException(
        'No se puede anular un documento REEMPLAZADO. Para auditoría, solicite la operación a SUPER_ADMIN.',
      );
    }

    return this.prisma.rrhh_legajo_documentos.update({
      where: { id: documentoId },
      data: {
        estado: 'ANULADO',
        motivo_anulacion: dto.motivo,
        anulado_por: userId,
        anulado_en: new Date(),
      },
    });
  }

  // ──────────────────────────────────────────────────────────────────────
  // Helpers privados
  // ──────────────────────────────────────────────────────────────────────

  private async cargarSiNoAnulado(documentoId: string, empresaId: string) {
    const doc = await this.prisma.rrhh_legajo_documentos.findFirst({
      where: { id: documentoId, empresa_id: empresaId },
    });
    if (!doc) throw new NotFoundException('Documento no encontrado');
    if (doc.estado === 'ANULADO') {
      throw new ForbiddenException(
        'El documento está anulado. Solicite acceso al historial al perfil SUPER_ADMIN o AUDITOR.',
      );
    }
    return doc;
  }

  private async validarEmpleado(empresaId: string, empleadoId: string) {
    const emp = await this.prisma.rrhh_empleados.findFirst({
      where: { id: empleadoId, empresa_id: empresaId },
      select: { id: true, estado: true },
    });
    if (!emp) throw new NotFoundException('Empleado no encontrado');
    return emp;
  }

  private async validarTipo(empresaId: string, tipoId: string) {
    const tipo = await this.prisma.rrhh_legajo_tipos_documento.findFirst({
      where: {
        id: tipoId,
        activo: true,
        OR: [{ empresa_id: null }, { empresa_id: empresaId }],
      },
    });
    if (!tipo) {
      throw new NotFoundException('Tipo de documento no encontrado o inactivo.');
    }
    return tipo;
  }

  private async validarTamanio(empresaId: string, sizeBytes: number) {
    const param = await this.parametros.obtenerVigente(empresaId, 'LEGAJO_TAMANIO_MAX_ARCHIVO_MB');
    const limitMb = param ? Number(param.valor) : 20;
    const limitBytes = (Number.isFinite(limitMb) ? limitMb : 20) * 1024 * 1024;
    if (sizeBytes > limitBytes) {
      throw new HttpException(
        {
          success: false,
          message: `El archivo excede el tamaño máximo permitido (${limitMb} MB).`,
        },
        HttpStatus.PAYLOAD_TOO_LARGE,
      );
    }
  }

  private async cargarMimeHabilitados(empresaId: string): Promise<string[]> {
    const param = await this.parametros.obtenerVigente(empresaId, 'LEGAJO_MIME_HABILITADOS');
    if (!param || !param.valor) {
      // Fallback razonable si no se sembró el parámetro.
      return [
        'application/pdf',
        'image/jpeg',
        'image/png',
        'image/webp',
        'application/msword',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      ];
    }
    return param.valor.split(',').map((s) => s.trim()).filter(Boolean);
  }

  private parseFechaVencimiento(
    raw: string | undefined,
    tipo: { admite_vencimiento: boolean; vencimiento_obligatorio: boolean; codigo: string },
  ): Date | null {
    if (!raw) {
      if (tipo.vencimiento_obligatorio) {
        throw new BadRequestException(
          `Este tipo de documento (${tipo.codigo}) requiere fecha_vencimiento.`,
        );
      }
      return null;
    }
    if (!tipo.admite_vencimiento) {
      throw new BadRequestException(
        `Este tipo de documento (${tipo.codigo}) no admite fecha de vencimiento.`,
      );
    }
    const d = new Date(raw);
    if (Number.isNaN(d.getTime())) {
      throw new BadRequestException('fecha_vencimiento inválida (formato esperado YYYY-MM-DD).');
    }
    return d;
  }

  private calcularEstadoVencimiento(
    fechaVenc: Date | null,
    diasAlertaCustom: number | null,
  ): LegajoVencimientoEstado {
    if (!fechaVenc) return LegajoVencimientoEstado.SIN_VENCIMIENTO;
    const ahora = new Date();
    const inicioHoy = new Date(ahora.getFullYear(), ahora.getMonth(), ahora.getDate());
    if (fechaVenc.getTime() <= inicioHoy.getTime()) return LegajoVencimientoEstado.VENCIDO;
    const dias = diasAlertaCustom ?? 30;
    const umbral = new Date(inicioHoy);
    umbral.setDate(umbral.getDate() + dias);
    return fechaVenc.getTime() <= umbral.getTime()
      ? LegajoVencimientoEstado.POR_VENCER
      : LegajoVencimientoEstado.VIGENTE;
  }

  private calcularHashSHA256(buffer: Buffer): string {
    return createHash('sha256').update(buffer).digest('hex');
  }

  private async detectarDuplicado(
    empresaId: string,
    empleadoId: string,
    tipoId: string,
    hash: string,
  ): Promise<boolean> {
    const dup = await this.prisma.rrhh_legajo_documentos.findFirst({
      where: {
        empresa_id: empresaId,
        empleado_id: empleadoId,
        tipo_documento_id: tipoId,
        hash_sha256: hash,
        estado: { in: ['ACTIVO', 'REEMPLAZADO'] },
      },
      select: { id: true },
    });
    return !!dup;
  }

  private expiraEnSeg(): number {
    return envs.legajoPresignedUrlExpirySec;
  }

  private serializar(doc: any) {
    return {
      ...doc,
      tamanio_bytes: typeof doc.tamanio_bytes === 'bigint' ? doc.tamanio_bytes.toString() : doc.tamanio_bytes,
    };
  }
}
