import {
  BadRequestException, ConflictException, Injectable, InternalServerErrorException,
  Logger, NotFoundException,
} from '@nestjs/common';
import { cifrar, descifrar, enmascarar } from 'src/common/utils/cifrado.util';
import { envs } from 'src/config';
import { PrismaService } from 'src/prisma/prisma.service';
import { AuditService } from 'src/audit/audit.service';
import { CreateCanalDto, UpdateCanalDto } from './dto/canal.dto';
import {
  type CanalWhatsApp, type CredencialesWhatsApp, type ProveedorWhatsApp,
  permiteAutomaticos,
} from './whatsapp/whatsapp-provider.interface';

const NOMBRE_CLAVE = 'CRM_ENCRYPTION_KEY';

/** Qué credencial necesita cada proveedor para poder funcionar. */
const REQUERIDAS: Record<string, (keyof CredencialesWhatsApp)[]> = {
  meta_cloud: ['access_token', 'app_secret'],
  evolution_cloud: ['base_url', 'instancia'],
  evolution_baileys: ['base_url', 'instancia'],
};

/**
 * Canales de mensajería (ticket CRM-22). Ver docs/spec-crm-360.md §6.6.
 *
 * Guarda las credenciales cifradas y **nunca las devuelve**: la lectura las
 * enmascara. El que descifra es este servicio, cuando el proveedor las necesita
 * para enviar o para validar una firma — el conector nunca ve el cifrado ni la
 * clave.
 */
@Injectable()
export class CanalesService {
  private readonly logger = new Logger(CanalesService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly auditService: AuditService,
  ) {}

  async findAll(empresa_id: string, tipo?: string) {
    const filas = await this.prisma.crm_canales.findMany({
      where: { empresa_id, ...(tipo ? { tipo } : {}) },
      orderBy: [{ tipo: 'asc' }, { nombre: 'asc' }],
    });
    return filas.map((c) => this.aVistaPublica(c));
  }

  async findOne(id: string, empresa_id: string) {
    return this.aVistaPublica(await this.deEmpresa(id, empresa_id));
  }

  async create(dto: CreateCanalDto, empresa_id: string, user_id?: string) {
    this.validarProveedorParaTipo(dto.tipo, dto.proveedor);
    this.validarCredenciales(dto.proveedor, dto.credenciales ?? {});

    const repetido = await this.prisma.crm_canales.findFirst({
      where: { empresa_id, tipo: dto.tipo, identificador: dto.identificador ?? null },
    });
    if (repetido) {
      throw new ConflictException(
        `Ya hay un canal de ${dto.tipo} con el identificador "${dto.identificador ?? '(vacío)'}"`,
      );
    }

    const canal = await this.prisma.crm_canales.create({
      data: {
        empresa_id,
        tipo: dto.tipo,
        proveedor: dto.proveedor,
        nombre: dto.nombre.trim(),
        identificador: dto.identificador?.trim() || null,
        identificador_externo: dto.identificador_externo?.trim() || null,
        webhook_verify_token: dto.webhook_verify_token?.trim() || null,
        credenciales_cifradas: dto.credenciales ? this.cifrarCredenciales(dto.credenciales) : null,
        equipo_id: dto.equipo_id ?? null,
        created_by: user_id ?? null,
      },
    });

    await this.auditar(empresa_id, user_id, 'CREAR', canal.id, undefined, this.aVistaPublica(canal));
    this.logger.log(`Canal creado: ${canal.tipo}/${canal.proveedor} "${canal.nombre}" (${canal.id})`);
    return this.aVistaPublica(canal);
  }

  async update(id: string, dto: UpdateCanalDto, empresa_id: string, user_id?: string) {
    const anterior = await this.deEmpresa(id, empresa_id);

    // Mezcla sobre lo guardado: el frontend manda sólo lo que se cambió, así no
    // hace falta volver a tipear el access token para corregir el nombre.
    let cifradas = anterior.credenciales_cifradas;
    if (dto.credenciales) {
      const fusionadas = { ...this.descifrarCredenciales(anterior.credenciales_cifradas), ...dto.credenciales };
      this.validarCredenciales(anterior.proveedor, fusionadas);
      cifradas = this.cifrarCredenciales(fusionadas);
    }

    const canal = await this.prisma.crm_canales.update({
      where: { id },
      data: {
        ...(dto.nombre !== undefined ? { nombre: dto.nombre.trim() } : {}),
        ...(dto.identificador !== undefined ? { identificador: dto.identificador?.trim() || null } : {}),
        ...(dto.identificador_externo !== undefined
          ? { identificador_externo: dto.identificador_externo?.trim() || null }
          : {}),
        ...(dto.webhook_verify_token !== undefined
          ? { webhook_verify_token: dto.webhook_verify_token?.trim() || null }
          : {}),
        ...(dto.equipo_id !== undefined ? { equipo_id: dto.equipo_id } : {}),
        ...(dto.activo !== undefined ? { activo: dto.activo } : {}),
        credenciales_cifradas: cifradas,
        updated_at: new Date(),
      },
    });

    await this.auditar(empresa_id, user_id, 'EDITAR', id, this.aVistaPublica(anterior), this.aVistaPublica(canal));
    return this.aVistaPublica(canal);
  }

  /**
   * Baja lógica. Borrar el canal dejaría las conversaciones apuntando a nada, y
   * son el historial de lo que se habló con cada cliente.
   */
  async remove(id: string, empresa_id: string, user_id?: string) {
    const anterior = await this.deEmpresa(id, empresa_id);
    const canal = await this.prisma.crm_canales.update({
      where: { id },
      data: { activo: false, updated_at: new Date() },
    });
    await this.auditar(empresa_id, user_id, 'ELIMINAR', id, this.aVistaPublica(anterior), this.aVistaPublica(canal));
    return this.aVistaPublica(canal);
  }

  // ── Para los proveedores y la ingesta ───────────────────────────────────────

  /** El canal con las credenciales ya descifradas. Uso interno: nunca sale por API. */
  async paraProveedor(id: string): Promise<CanalWhatsApp> {
    const canal = await this.prisma.crm_canales.findUnique({ where: { id } });
    if (!canal) throw new NotFoundException('Canal no encontrado');
    return this.aCanalWhatsApp(canal);
  }

  /**
   * Resuelve el canal por el id del proveedor, que es lo único de confianza que
   * trae un webhook antes de validar la firma.
   */
  async porIdentificadorExterno(identificador_externo: string): Promise<CanalWhatsApp | null> {
    const canal = await this.prisma.crm_canales.findFirst({
      where: { identificador_externo, activo: true },
    });
    return canal ? this.aCanalWhatsApp(canal) : null;
  }

  /** Para el GET de verificación de Meta, que no trae cuerpo ni firma. */
  porVerifyToken(webhook_verify_token: string) {
    return this.prisma.crm_canales.findFirst({
      where: { webhook_verify_token, activo: true },
      select: { id: true, webhook_verify_token: true },
    });
  }

  /** Deja constancia de que el canal respondió, o de por qué no. */
  async marcarEstado(id: string, ok: boolean, error?: string) {
    await this.prisma.crm_canales.update({
      where: { id },
      data: {
        ...(ok ? { verificado_at: new Date(), ultimo_error: null } : { ultimo_error: error ?? 'Error desconocido' }),
        updated_at: new Date(),
      },
    });
  }

  // ---------------------------------------------------------------------------

  private aCanalWhatsApp(canal: {
    id: string;
    empresa_id: string;
    proveedor: string;
    identificador: string | null;
    identificador_externo: string | null;
    credenciales_cifradas: string | null;
  }): CanalWhatsApp {
    return {
      id: canal.id,
      empresa_id: canal.empresa_id,
      proveedor: canal.proveedor as ProveedorWhatsApp,
      identificador: canal.identificador,
      identificador_externo: canal.identificador_externo,
      credenciales: this.descifrarCredenciales(canal.credenciales_cifradas),
    };
  }

  /** Lo que sí puede salir por API: sin credenciales, con el aviso de Baileys. */
  private aVistaPublica(canal: {
    id: string;
    empresa_id: string;
    tipo: string;
    proveedor: string;
    nombre: string;
    identificador: string | null;
    identificador_externo: string | null;
    webhook_verify_token: string | null;
    credenciales_cifradas: string | null;
    equipo_id: string | null;
    activo: boolean;
    verificado_at: Date | null;
    ultimo_error: string | null;
  }) {
    const cred = canal.credenciales_cifradas ? this.descifrarSeguro(canal.credenciales_cifradas) : {};
    return {
      id: canal.id,
      tipo: canal.tipo,
      proveedor: canal.proveedor,
      nombre: canal.nombre,
      identificador: canal.identificador,
      identificador_externo: canal.identificador_externo,
      webhook_verify_token: canal.webhook_verify_token,
      equipo_id: canal.equipo_id,
      activo: canal.activo,
      verificado_at: canal.verificado_at,
      ultimo_error: canal.ultimo_error,
      credenciales_configuradas: Boolean(canal.credenciales_cifradas),
      credenciales: {
        access_token: enmascarar(cred.access_token),
        app_secret: enmascarar(cred.app_secret),
        api_key: enmascarar(cred.api_key),
        base_url: cred.base_url ?? null,
        instancia: cred.instancia ?? null,
      },
      permite_envios_automaticos: permiteAutomaticos(canal.proveedor as ProveedorWhatsApp),
    };
  }

  private cifrarCredenciales(credenciales: CredencialesWhatsApp): string {
    const clave = envs.crmEncryptionKey ?? '';
    if (!clave) {
      throw new InternalServerErrorException(
        `Falta ${NOMBRE_CLAVE} en el entorno: sin eso no se pueden guardar credenciales de canal`,
      );
    }
    // Sólo lo que tiene valor: guardar claves vacías ensucia el descifrado.
    const limpias = Object.fromEntries(
      Object.entries(credenciales).filter(([, v]) => typeof v === 'string' && v.trim() !== ''),
    );
    return cifrar(JSON.stringify(limpias), clave, NOMBRE_CLAVE);
  }

  private descifrarCredenciales(cifradas: string | null): CredencialesWhatsApp {
    if (!cifradas) return {};
    const clave = envs.crmEncryptionKey ?? '';
    if (!clave) {
      throw new InternalServerErrorException(`Falta ${NOMBRE_CLAVE} en el entorno: no se pueden leer las credenciales`);
    }
    return JSON.parse(descifrar(cifradas, clave, NOMBRE_CLAVE)) as CredencialesWhatsApp;
  }

  /**
   * Para el listado: si la clave cambió o falta, una credencial ilegible no debe
   * tumbar la pantalla entera. Se devuelve vacía y el flag
   * `credenciales_configuradas` sigue diciendo que hay algo guardado.
   */
  private descifrarSeguro(cifradas: string): CredencialesWhatsApp {
    try {
      return this.descifrarCredenciales(cifradas);
    } catch {
      this.logger.warn('No se pudieron descifrar las credenciales de un canal: ¿cambió CRM_ENCRYPTION_KEY?');
      return {};
    }
  }

  private validarProveedorParaTipo(tipo: string, proveedor: string) {
    const validos: Record<string, string[]> = {
      whatsapp: ['meta_cloud', 'evolution_cloud', 'evolution_baileys'],
      email: ['imap_smtp'],
      instagram: ['meta_cloud'],
      facebook: ['meta_cloud'],
      webchat: ['webchat_propio'],
    };
    if (!validos[tipo]?.includes(proveedor)) {
      throw new BadRequestException(
        `El proveedor "${proveedor}" no sirve para un canal de ${tipo}. Opciones: ${validos[tipo]?.join(', ')}`,
      );
    }
  }

  /** Mejor rechazar al crear que descubrirlo cuando falle el primer envío. */
  private validarCredenciales(proveedor: string, credenciales: CredencialesWhatsApp) {
    const faltantes = (REQUERIDAS[proveedor] ?? []).filter((k) => {
      const v = credenciales[k];
      return typeof v !== 'string' || v.trim() === '';
    });
    if (faltantes.length) {
      throw new BadRequestException({
        message: `Para ${proveedor} faltan credenciales: ${faltantes.join(', ')}`,
        campos_faltantes: faltantes,
      });
    }
  }

  private async deEmpresa(id: string, empresa_id: string) {
    const canal = await this.prisma.crm_canales.findFirst({ where: { id, empresa_id } });
    if (!canal) throw new NotFoundException('Canal no encontrado en esta empresa');
    return canal;
  }

  private auditar(
    empresa_id: string, user_id: string | undefined, action: string,
    entity_id: string, old_value?: unknown, new_value?: unknown,
  ) {
    return this.auditService.log({
      empresa_id, user_id, action, entity_type: 'crm_canales', entity_id,
      old_value: old_value as never, new_value: new_value as never,
    });
  }
}
