import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { resolverSelectores, type PrivilegioSelector } from './selectores';
import {
  PERFILES_SISTEMA_SEED,
  SEGURIDAD_SEED,
  SeedModulo,
  SeedPerfil,
  SeedSubmodulo,
} from './seguridad.seed-data';

type IngestaResultado = {
  modulos_creados: number;
  modulos_actualizados: number;
  submodulos_creados: number;
  submodulos_actualizados: number;
  privilegios_creados: number;
  privilegios_actualizados: number;
  perfiles_creados: number;
  perfiles_omitidos: number;
};

/**
 * Orchestrator idempotente del seed de seguridad (Fase 1).
 *
 *  - Upsert por `codigo` para módulos, submódulos y privilegios.
 *  - NO borra nada. NO toca `perfiles_privilegios` legacy.
 *  - El TRUNCATE/reseed destructivo (Decisión 6) se hace después en una
 *    operación separada bajo confirmación explícita.
 *  - Perfiles del sistema se siembran POR EMPRESA bajo demanda
 *    (botón "Cargar perfiles sugeridos", Decisión 4.2).
 */
// Límites de columna de `perfiles` (perfil VarChar(50), desc_perfil VarChar(100)).
// Truncar en el borde evita que una descripción larga rompa el INSERT.
const MAX_PERFIL = 50;
const MAX_DESC = 100;
const nombrePerfil = (s: string) => s.slice(0, MAX_PERFIL);
const descPerfil = (s: string) => s.slice(0, MAX_DESC);

@Injectable()
export class SeguridadSeedOrchestrator {
  private readonly logger = new Logger(SeguridadSeedOrchestrator.name);

  constructor(private readonly prisma: PrismaService) {}

  /** Siembra el catálogo global (módulos, submódulos, privilegios). */
  async runCatalogoMaestro(): Promise<IngestaResultado> {
    const result: IngestaResultado = {
      modulos_creados: 0,
      modulos_actualizados: 0,
      submodulos_creados: 0,
      submodulos_actualizados: 0,
      privilegios_creados: 0,
      privilegios_actualizados: 0,
      perfiles_creados: 0,
      perfiles_omitidos: 0,
    };

    for (let i = 0; i < SEGURIDAD_SEED.length; i++) {
      const mod = SEGURIDAD_SEED[i];
      await this.upsertModulo(mod, i, result);
    }

    this.logger.log(
      `Catálogo seed: ${JSON.stringify({
        m: `${result.modulos_creados}+${result.modulos_actualizados}`,
        sm: `${result.submodulos_creados}+${result.submodulos_actualizados}`,
        pr: `${result.privilegios_creados}+${result.privilegios_actualizados}`,
      })}`,
    );

    return result;
  }

  private async upsertModulo(
    mod: SeedModulo,
    orden: number,
    result: IngestaResultado,
  ): Promise<void> {
    const existing = await this.prisma.modulos.findFirst({
      where: { codigo: mod.codigo },
      select: { id: true },
    });

    let moduloId: string;
    if (!existing) {
      const created = await this.prisma.modulos.create({
        data: {
          codigo: mod.codigo,
          descripcion: mod.descripcion,
          icono: mod.icono,
          orden,
          active: true,
        },
      });
      moduloId = created.id;
      result.modulos_creados++;
    } else {
      await this.prisma.modulos.update({
        where: { id: existing.id },
        data: {
          descripcion: mod.descripcion,
          icono: mod.icono,
          orden,
          active: true,
        },
      });
      moduloId = existing.id;
      result.modulos_actualizados++;
    }

    for (let i = 0; i < mod.submodulos.length; i++) {
      await this.upsertSubmodulo(moduloId, mod.submodulos[i], i, result);
    }
  }

  private async upsertSubmodulo(
    moduloId: string,
    sub: SeedSubmodulo,
    orden: number,
    result: IngestaResultado,
  ): Promise<void> {
    const submodulo = await this.prisma.submodulos.upsert({
      where: { codigo: sub.codigo },
      create: {
        modulo_id: moduloId,
        codigo: sub.codigo,
        descripcion: sub.descripcion,
        icono: sub.icono,
        orden,
        es_addon: !!sub.es_addon,
        precio_addon: sub.precio_addon ?? null,
        active: true,
      },
      update: {
        modulo_id: moduloId,
        descripcion: sub.descripcion,
        icono: sub.icono,
        orden,
        es_addon: !!sub.es_addon,
        precio_addon: sub.precio_addon ?? null,
        active: true,
      },
    });

    if (
      submodulo.created_at &&
      submodulo.updated_at &&
      submodulo.created_at.getTime() === submodulo.updated_at.getTime()
    ) {
      result.submodulos_creados++;
    } else {
      result.submodulos_actualizados++;
    }

    for (const priv of sub.privilegios) {
      const existing = await this.prisma.privilegios.findFirst({
        where: { codigo: priv.codigo },
        select: { id: true },
      });

      if (!existing) {
        await this.prisma.privilegios.create({
          data: {
            codigo: priv.codigo,
            desc_privilegio: priv.desc,
            submodulo_id: submodulo.id,
            recurso: priv.recurso,
            accion: priv.accion,
            is_sistema: true,
            active: true,
          },
        });
        result.privilegios_creados++;
      } else {
        await this.prisma.privilegios.update({
          where: { id: existing.id },
          data: {
            desc_privilegio: priv.desc,
            submodulo_id: submodulo.id,
            recurso: priv.recurso,
            accion: priv.accion,
            is_sistema: true,
            active: true,
          },
        });
        result.privilegios_actualizados++;
      }
    }
  }

  /**
   * Siembra los perfiles de sistema para UNA empresa específica.
   * Idempotente: si el perfil ya existe con `is_sistema=true`, se omite.
   *
   * Nota: `perfiles.is_sistema` aún no existe como columna. Hasta que se
   * agregue (Fase 2/migración aparte), usamos el prefijo "[SISTEMA] " en el
   * nombre como marcador.
   */
  async runPerfilesSistemaParaEmpresa(empresaId: string): Promise<IngestaResultado> {
    const result: IngestaResultado = {
      modulos_creados: 0,
      modulos_actualizados: 0,
      submodulos_creados: 0,
      submodulos_actualizados: 0,
      privilegios_creados: 0,
      privilegios_actualizados: 0,
      perfiles_creados: 0,
      perfiles_omitidos: 0,
    };

    const todosLosPrivilegios = await this.loadSystemPrivileges();

    const privIdToModuloId = new Map(
      todosLosPrivilegios
        .filter((p) => p.submodulos?.modulo_id)
        .map((p) => [p.id, p.submodulos.modulo_id]),
    );

    const todosLosModulos = await this.prisma.modulos.findMany({
      select: { id: true, codigo: true },
    });

    for (const perfilSeed of PERFILES_SISTEMA_SEED) {
      const privilegioIds = this.resolverSelectores(
        perfilSeed,
        todosLosPrivilegios,
        todosLosModulos,
      );

      const existente = await this.prisma.perfiles.findFirst({
        where: {
          empresa_id: empresaId,
          perfil: perfilSeed.perfil,
          is_sistema: true,
        },
        select: {
          id: true,
          perfiles_privilegios: { select: { privilegio_id: true } },
        },
      });

      if (existente) {
        const yaAsignados = new Set(
          existente.perfiles_privilegios.map((pp) => pp.privilegio_id),
        );
        const faltantes = privilegioIds.filter((id) => !yaAsignados.has(id));
        if (faltantes.length > 0) {
          await this.prisma.perfiles_privilegios.createMany({
            data: faltantes.map((privilegio_id) => ({
              perfil_id: existente.id,
              privilegio_id,
              modulo_id: privIdToModuloId.get(privilegio_id) ?? null,
              active: true,
            })),
          });
        }
        result.perfiles_omitidos++;
        continue;
      }

      const perfilCreado = await this.prisma.perfiles.create({
        data: {
          empresa_id: empresaId,
          perfil: nombrePerfil(perfilSeed.perfil),
          desc_perfil: descPerfil(perfilSeed.desc),
          is_sistema: true,
        },
      });

      if (privilegioIds.length > 0) {
        await this.prisma.perfiles_privilegios.createMany({
          data: privilegioIds.map((privilegio_id) => ({
            perfil_id: perfilCreado.id,
            privilegio_id,
            modulo_id: privIdToModuloId.get(privilegio_id) ?? null,
            active: true,
          })),
        });
      }

      result.perfiles_creados++;
    }

    this.logger.log(
      `Perfiles sistema empresa=${empresaId}: creados=${result.perfiles_creados} omitidos=${result.perfiles_omitidos}`,
    );

    return result;
  }

  /**
   * Sincronización automática (bootstrap): lleva los cambios de catálogo a los
   * perfiles del sistema YA vivos, sin pasos manuales y sin resucitar perfiles
   * que una empresa borró a propósito. Para cada empresa que ya usa perfiles del
   * sistema:
   *   1. Backfillea los privilegios faltantes de sus perfiles del sistema
   *      existentes (así "Administrador" u otros ganan los permisos nuevos).
   *   2. Crea los perfiles marcados `autoProvision` que todavía no tenga (p. ej.
   *      los perfiles operativos de ecommerce) con sus privilegios.
   * Las empresas que nunca cargaron perfiles del sistema quedan intactas.
   */
  async sincronizarPerfilesSistema(): Promise<{
    empresas: number;
    perfiles_creados: number;
    privilegios_agregados: number;
  }> {
    const privilegios = await this.loadSystemPrivileges();
    const privIdToModuloId = new Map(
      privilegios.filter((p) => p.submodulos?.modulo_id).map((p) => [p.id, p.submodulos.modulo_id]),
    );
    const modulos = await this.prisma.modulos.findMany({ select: { id: true, codigo: true } });

    // Empresas que ya usan perfiles del sistema (opt-in previo).
    const perfilesSistema = await this.prisma.perfiles.findMany({
      where: { is_sistema: true },
      select: {
        id: true,
        empresa_id: true,
        perfil: true,
        perfiles_privilegios: { select: { privilegio_id: true } },
      },
    });

    const empresasConSistema = new Set(
      perfilesSistema.map((p) => p.empresa_id).filter((e): e is string => !!e),
    );
    const result = { empresas: empresasConSistema.size, perfiles_creados: 0, privilegios_agregados: 0 };

    // 1. Backfill de perfiles existentes.
    for (const perfil of perfilesSistema) {
      const seed = PERFILES_SISTEMA_SEED.find((p) => p.perfil === perfil.perfil);
      if (!seed) continue;
      const objetivo = this.resolverSelectores(seed, privilegios, modulos);
      const yaAsignados = new Set(perfil.perfiles_privilegios.map((pp) => pp.privilegio_id));
      const faltantes = objetivo.filter((id) => !yaAsignados.has(id));
      if (faltantes.length > 0) {
        await this.prisma.perfiles_privilegios.createMany({
          data: faltantes.map((privilegio_id) => ({
            perfil_id: perfil.id,
            privilegio_id,
            modulo_id: privIdToModuloId.get(privilegio_id) ?? null,
            active: true,
          })),
        });
        result.privilegios_agregados += faltantes.length;
      }
    }

    // 2. Crear los perfiles autoProvision faltantes, sólo en empresas opt-in.
    const autoPerfiles = PERFILES_SISTEMA_SEED.filter((p) => p.autoProvision);
    for (const empresaId of empresasConSistema) {
      const nombresExistentes = new Set(
        perfilesSistema.filter((p) => p.empresa_id === empresaId).map((p) => p.perfil),
      );
      for (const seed of autoPerfiles) {
        if (nombresExistentes.has(seed.perfil)) continue;
        const privilegioIds = this.resolverSelectores(seed, privilegios, modulos);
        const creado = await this.prisma.perfiles.create({
          data: {
            empresa_id: empresaId,
            perfil: nombrePerfil(seed.perfil),
            desc_perfil: descPerfil(seed.desc),
            is_sistema: true,
          },
        });
        if (privilegioIds.length > 0) {
          await this.prisma.perfiles_privilegios.createMany({
            data: privilegioIds.map((privilegio_id) => ({
              perfil_id: creado.id,
              privilegio_id,
              modulo_id: privIdToModuloId.get(privilegio_id) ?? null,
              active: true,
            })),
          });
        }
        result.perfiles_creados++;
      }
    }

    this.logger.log(
      `Sync perfiles sistema: empresas=${result.empresas} perfiles_creados=${result.perfiles_creados} privilegios_agregados=${result.privilegios_agregados}`,
    );
    return result;
  }

  /**
   * Provisiona bajo demanda los perfiles operativos de ecommerce (`autoProvision`)
   * para UNA empresa: crea los que falten y backfillea los que ya estén. A
   * diferencia de "cargar sugeridos" (que crea TODOS los perfiles del sistema),
   * esto crea sólo los de la tienda — pensado para el botón del panel Ecommerce.
   */
  async provisionarPerfilesEcommerce(
    empresaId: string,
  ): Promise<{ creados: number; privilegios_agregados: number }> {
    const privilegios = await this.loadSystemPrivileges();
    const privIdToModuloId = new Map(
      privilegios.filter((p) => p.submodulos?.modulo_id).map((p) => [p.id, p.submodulos.modulo_id]),
    );
    const modulos = await this.prisma.modulos.findMany({ select: { id: true, codigo: true } });
    const autoPerfiles = PERFILES_SISTEMA_SEED.filter((p) => p.autoProvision);

    const existentes = await this.prisma.perfiles.findMany({
      where: {
        empresa_id: empresaId,
        is_sistema: true,
        perfil: { in: autoPerfiles.map((p) => p.perfil) },
      },
      select: { id: true, perfil: true, perfiles_privilegios: { select: { privilegio_id: true } } },
    });
    const porNombre = new Map(existentes.map((e) => [e.perfil, e]));

    let creados = 0;
    let agregados = 0;
    for (const seed of autoPerfiles) {
      const objetivo = this.resolverSelectores(seed, privilegios, modulos);
      const existente = porNombre.get(seed.perfil);
      if (existente) {
        const ya = new Set(existente.perfiles_privilegios.map((pp) => pp.privilegio_id));
        const faltantes = objetivo.filter((id) => !ya.has(id));
        if (faltantes.length > 0) {
          await this.prisma.perfiles_privilegios.createMany({
            data: faltantes.map((privilegio_id) => ({
              perfil_id: existente.id,
              privilegio_id,
              modulo_id: privIdToModuloId.get(privilegio_id) ?? null,
              active: true,
            })),
          });
          agregados += faltantes.length;
        }
        continue;
      }
      const creado = await this.prisma.perfiles.create({
        data: { empresa_id: empresaId, perfil: nombrePerfil(seed.perfil), desc_perfil: descPerfil(seed.desc), is_sistema: true },
      });
      if (objetivo.length > 0) {
        await this.prisma.perfiles_privilegios.createMany({
          data: objetivo.map((privilegio_id) => ({
            perfil_id: creado.id,
            privilegio_id,
            modulo_id: privIdToModuloId.get(privilegio_id) ?? null,
            active: true,
          })),
        });
      }
      creados++;
    }

    this.logger.log(
      `Perfiles ecommerce empresa=${empresaId}: creados=${creados} privilegios_agregados=${agregados}`,
    );
    return { creados, privilegios_agregados: agregados };
  }

  /**
   * Carga los privilegios de sistema activos con su submódulo y módulo vía SQL.
   * Motivo: hay filas legacy con `submodulo_id` NULL y el schema Prisma lo tipa
   * como no-nullable, así que CUALQUIER lectura ORM de esas filas (incluso al
   * resolver la relación) rompe con "Error converting field submodulo_id". El
   * INNER JOIN las descarta naturalmente — un privilegio sin submódulo es un
   * huérfano que no pertenece a ningún módulo y no debe asignarse a un perfil.
   */
  private async loadSystemPrivileges(): Promise<
    Array<{
      id: string;
      codigo: string | null;
      accion: string | null;
      submodulos: { codigo: string; modulo_id: string } | null;
    }>
  > {
    const rows = await this.prisma.$queryRaw<
      Array<{ id: string; codigo: string | null; accion: string | null; sub_codigo: string; modulo_id: string }>
    >`
      SELECT p.id, p.codigo, p.accion, s.codigo AS sub_codigo, s.modulo_id
      FROM privilegios p
      JOIN submodulos s ON s.id = p.submodulo_id
      WHERE p.is_sistema = true AND p.active = true
    `;
    return rows.map((r) => ({
      id: r.id,
      codigo: r.codigo,
      accion: r.accion,
      submodulos: { codigo: r.sub_codigo, modulo_id: r.modulo_id },
    }));
  }

  private resolverSelectores(
    perfil: SeedPerfil,
    privilegios: PrivilegioSelector[],
    modulos: Array<{ id: string; codigo: string | null }>,
  ): string[] {
    return resolverSelectores(perfil.selectores, privilegios, modulos);
  }
}
