import {
  BadRequestException,
  forwardRef,
  Inject,
  Injectable,
  InternalServerErrorException,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
import { envs } from '../../config';
import { PrismaService } from '../../prisma/prisma.service';
import {
  CreateRelojDto,
  QueryRelojesDto,
  RelojProtocoloApi,
  RelojTipoAuth,
  RelojTipoConexion,
  UpdateRelojDto,
} from '../dto/relojes.dto';
import { MarcacionesService } from './marcaciones.service';
import { PresentismoSyncSchedulerService } from './presentismo-sync-scheduler.service';

const ALGORITHM = 'aes-256-cbc';
const IV_LENGTH = 16;
const ENCRYPTED_PREFIX = 'enc:v1:';

function getEncryptionKey(): Buffer {
  const raw = envs.aiEncryptionKey ?? '';
  if (raw.length !== 32) {
    throw new InternalServerErrorException(
      'AI_ENCRYPTION_KEY debe tener exactamente 32 caracteres para cifrar credenciales de relojes',
    );
  }
  return Buffer.from(raw, 'utf8');
}

function encryptApiKey(plain: string): string {
  const key = getEncryptionKey();
  const iv = randomBytes(IV_LENGTH);
  const cipher = createCipheriv(ALGORITHM, key, iv);
  const encrypted = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]);
  return `${ENCRYPTED_PREFIX}${iv.toString('hex')}:${encrypted.toString('hex')}`;
}

function decryptApiKey(stored: string): string {
  if (!stored.startsWith(ENCRYPTED_PREFIX)) {
    // Compatibilidad: si nunca fue cifrada (no debería ocurrir), la devuelve tal cual.
    return stored;
  }
  const payload = stored.slice(ENCRYPTED_PREFIX.length);
  const [ivHex, dataHex] = payload.split(':');
  const key = getEncryptionKey();
  const iv = Buffer.from(ivHex, 'hex');
  const data = Buffer.from(dataHex, 'hex');
  const decipher = createDecipheriv(ALGORITHM, key, iv);
  const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
  return decrypted.toString('utf8');
}

function maskApiKey(stored: string | null): string | null {
  if (!stored) return null;
  return '••••••••';
}

function sanitizeReloj<
  T extends { api_key: string | null },
>(reloj: T): Omit<T, 'api_key'> & { api_key: string | null; tiene_credencial: boolean } {
  const { api_key, ...rest } = reloj;
  return {
    ...rest,
    api_key: maskApiKey(api_key),
    tiene_credencial: Boolean(api_key),
  } as Omit<T, 'api_key'> & { api_key: string | null; tiene_credencial: boolean };
}

@Injectable()
export class RelojesMarcadoresService {
  constructor(
    private readonly prisma: PrismaService,
    @Inject(forwardRef(() => MarcacionesService))
    private readonly marcaciones: MarcacionesService,
    private readonly scheduler: PresentismoSyncSchedulerService,
  ) {}

  async listar(empresaId: string, query: QueryRelojesDto) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;

    const where: Prisma.rrhh_relojes_marcadoresWhereInput = { empresa_id: empresaId };
    if (query.tipo_conexion) where.tipo_conexion = query.tipo_conexion as any;
    if (query.sucursal_id) where.sucursal_id = query.sucursal_id;
    if (typeof query.activo === 'boolean') where.activo = query.activo;

    const [data, total] = await this.prisma.$transaction([
      this.prisma.rrhh_relojes_marcadores.findMany({
        where,
        skip,
        take: limit,
        orderBy: [{ activo: 'desc' }, { nombre: 'asc' }],
        include: {
          sucursal: { select: { id: true, descripcion: true } },
        },
      }),
      this.prisma.rrhh_relojes_marcadores.count({ where }),
    ]);

    return {
      data: data.map(sanitizeReloj),
      total,
      page,
      limit,
    };
  }

  async obtener(id: string, empresaId: string) {
    const reloj = await this.prisma.rrhh_relojes_marcadores.findFirst({
      where: { id, empresa_id: empresaId },
      include: { sucursal: { select: { id: true, descripcion: true } } },
    });
    if (!reloj) throw new NotFoundException('Reloj marcador no encontrado');
    return sanitizeReloj(reloj);
  }

  async crear(empresaId: string, dto: CreateRelojDto, userId?: string) {
    await this.validarSucursal(empresaId, dto.sucursal_id);
    this.validarPayloadSegunModalidad(dto);

    const reloj = await this.prisma.rrhh_relojes_marcadores.create({
      data: {
        empresa_id: empresaId,
        sucursal_id: dto.sucursal_id ?? null,
        nombre: dto.nombre,
        tipo_conexion: dto.tipo_conexion as any,
        protocolo_api:
          dto.tipo_conexion === RelojTipoConexion.API
            ? (dto.protocolo_api as any) ?? RelojProtocoloApi.GENERICO
            : RelojProtocoloApi.GENERICO,
        url_base: dto.tipo_conexion === RelojTipoConexion.API ? dto.url_base ?? null : null,
        api_key:
          dto.tipo_conexion === RelojTipoConexion.API && dto.api_key
            ? encryptApiKey(dto.api_key)
            : null,
        tipo_auth: dto.tipo_conexion === RelojTipoConexion.API ? (dto.tipo_auth as any) ?? null : null,
        formato_respuesta:
          dto.tipo_conexion === RelojTipoConexion.API ? dto.formato_respuesta ?? null : null,
        campo_documento:
          dto.tipo_conexion === RelojTipoConexion.API ? dto.campo_documento ?? null : null,
        campo_timestamp:
          dto.tipo_conexion === RelojTipoConexion.API ? dto.campo_timestamp ?? null : null,
        intervalo_polling_min:
          dto.tipo_conexion === RelojTipoConexion.API ? dto.intervalo_polling_min ?? null : null,
        activo: dto.activo ?? true,
        creado_por: userId ?? null,
      },
      include: { sucursal: { select: { id: true, descripcion: true } } },
    });

    await this.scheduler.reconciliarReloj(reloj.id);
    return sanitizeReloj(reloj);
  }

  async actualizar(id: string, empresaId: string, dto: UpdateRelojDto) {
    const reloj = await this.prisma.rrhh_relojes_marcadores.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!reloj) throw new NotFoundException('Reloj marcador no encontrado');

    if (dto.sucursal_id !== undefined) {
      await this.validarSucursal(empresaId, dto.sucursal_id);
    }

    const tipoFinal = (dto.tipo_conexion ?? reloj.tipo_conexion) as RelojTipoConexion;
    const merged: CreateRelojDto = {
      nombre: dto.nombre ?? reloj.nombre,
      tipo_conexion: tipoFinal,
      protocolo_api: (dto.protocolo_api ?? reloj.protocolo_api ?? undefined) as any,
      url_base: dto.url_base ?? reloj.url_base ?? undefined,
      api_key: dto.api_key,
      tipo_auth: (dto.tipo_auth ?? reloj.tipo_auth ?? undefined) as any,
      formato_respuesta: (dto.formato_respuesta ?? reloj.formato_respuesta ?? undefined) as any,
      campo_documento: dto.campo_documento ?? reloj.campo_documento ?? undefined,
      campo_timestamp: dto.campo_timestamp ?? reloj.campo_timestamp ?? undefined,
      intervalo_polling_min: dto.intervalo_polling_min ?? reloj.intervalo_polling_min ?? undefined,
      sucursal_id: dto.sucursal_id ?? reloj.sucursal_id ?? undefined,
      activo: dto.activo ?? reloj.activo,
    };
    this.validarPayloadSegunModalidad(merged);

    const data: Prisma.rrhh_relojes_marcadoresUncheckedUpdateInput = {
      updated_at: new Date(),
    };
    if (dto.nombre !== undefined) data.nombre = dto.nombre;
    if (dto.sucursal_id !== undefined) data.sucursal_id = dto.sucursal_id;
    if (dto.tipo_conexion !== undefined) data.tipo_conexion = dto.tipo_conexion as any;
    if (dto.protocolo_api !== undefined) data.protocolo_api = dto.protocolo_api as any;
    if (dto.url_base !== undefined) data.url_base = dto.url_base ?? null;
    if (dto.tipo_auth !== undefined) data.tipo_auth = (dto.tipo_auth ?? null) as any;
    if (dto.formato_respuesta !== undefined) data.formato_respuesta = dto.formato_respuesta ?? null;
    if (dto.campo_documento !== undefined) data.campo_documento = dto.campo_documento ?? null;
    if (dto.campo_timestamp !== undefined) data.campo_timestamp = dto.campo_timestamp ?? null;
    if (dto.intervalo_polling_min !== undefined)
      data.intervalo_polling_min = dto.intervalo_polling_min ?? null;
    if (dto.activo !== undefined) data.activo = dto.activo;
    if (dto.api_key !== undefined) {
      data.api_key = dto.api_key ? encryptApiKey(dto.api_key) : null;
    }

    // Si pasamos a PLANILLA, limpiamos campos de API.
    if (tipoFinal === RelojTipoConexion.PLANILLA) {
      data.protocolo_api = RelojProtocoloApi.GENERICO as any;
      data.url_base = null;
      data.tipo_auth = null;
      data.formato_respuesta = null;
      data.campo_documento = null;
      data.campo_timestamp = null;
      data.intervalo_polling_min = null;
      data.api_key = null;
    }

    const updated = await this.prisma.rrhh_relojes_marcadores.update({
      where: { id },
      data,
      include: { sucursal: { select: { id: true, descripcion: true } } },
    });

    await this.scheduler.reconciliarReloj(updated.id);
    return sanitizeReloj(updated);
  }

  async eliminar(id: string, empresaId: string) {
    const reloj = await this.prisma.rrhh_relojes_marcadores.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!reloj) throw new NotFoundException('Reloj marcador no encontrado');

    const enUso = await this.prisma.rrhh_marcaciones_raw.findFirst({
      where: { reloj_id: id },
      select: { id: true },
    });
    if (enUso) {
      // No se borra: se desactiva para no perder trazabilidad de las marcaciones.
      const desactivado = await this.prisma.rrhh_relojes_marcadores.update({
        where: { id },
        data: { activo: false, updated_at: new Date() },
        include: { sucursal: { select: { id: true, descripcion: true } } },
      });
      await this.scheduler.reconciliarReloj(desactivado.id);
      return {
        eliminado: false,
        desactivado: true,
        reloj: sanitizeReloj(desactivado),
      };
    }

    await this.prisma.rrhh_relojes_marcadores.delete({ where: { id } });
    await this.scheduler.reconciliarReloj(id);
    return { eliminado: true, desactivado: false };
  }

  /**
   * Ejecuta la sincronización con el reloj (Fase 2 M16).
   * Consulta el endpoint del reloj, ingiere las marcaciones en raw,
   * dispara deduplicación y devuelve el resumen.
   */
  async sincronizar(
    id: string,
    empresaId: string,
    opts: { desde?: Date; hasta?: Date; userId?: string },
  ) {
    const reloj = await this.prisma.rrhh_relojes_marcadores.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!reloj) throw new NotFoundException('Reloj marcador no encontrado');
    if (reloj.tipo_conexion !== 'API') {
      throw new BadRequestException(
        'La sincronización vía API sólo aplica a relojes con tipo_conexion = API',
      );
    }
    if (!reloj.activo) {
      throw new BadRequestException('El reloj está inactivo');
    }
    const apiKey = reloj.api_key ? decryptApiKey(reloj.api_key) : null;
    return this.marcaciones.sincronizarReloj(empresaId, id, apiKey, opts);
  }

  /**
   * Devuelve la api_key en claro (uso interno del cliente HTTP de Fase 2).
   * NO debe exponerse por endpoint público.
   */
  async revelarApiKey(id: string, empresaId: string): Promise<string | null> {
    const reloj = await this.prisma.rrhh_relojes_marcadores.findFirst({
      where: { id, empresa_id: empresaId },
      select: { api_key: true },
    });
    if (!reloj) throw new NotFoundException('Reloj marcador no encontrado');
    return reloj.api_key ? decryptApiKey(reloj.api_key) : null;
  }

  private validarPayloadSegunModalidad(dto: CreateRelojDto) {
    if (dto.tipo_conexion === RelojTipoConexion.API) {
      if (!dto.url_base) {
        throw new BadRequestException('url_base es obligatorio para relojes tipo API');
      }
      if (!dto.tipo_auth) {
        throw new BadRequestException('tipo_auth es obligatorio para relojes tipo API');
      }
      // Regla 2: Digest requiere credencial usuario:password.
      if (dto.tipo_auth === RelojTipoAuth.DIGEST && dto.api_key && !dto.api_key.includes(':')) {
        throw new BadRequestException('Digest requiere api_key con formato "usuario:password"');
      }
    } else if (
      dto.protocolo_api &&
      dto.protocolo_api !== RelojProtocoloApi.GENERICO
    ) {
      // Regla 1: el protocolo HIKVISION_ISAPI sólo aplica a relojes API.
      throw new BadRequestException(
        'protocolo_api distinto de GENERICO sólo aplica a relojes con tipo_conexion = API',
      );
    }
  }

  private async validarSucursal(empresaId: string, sucursalId?: string | null) {
    if (!sucursalId) return;
    const sucursal = await this.prisma.empresas_sucursales.findFirst({
      where: { id: sucursalId, empresa_id: empresaId },
      select: { id: true },
    });
    if (!sucursal) {
      throw new BadRequestException('La sucursal no pertenece a la empresa');
    }
  }
}
