import {
  Injectable,
  InternalServerErrorException,
  UnauthorizedException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { AdminUser, AdminUserRole } from "@prisma/client";
import * as bcrypt from "bcrypt";
import { randomBytes } from "node:crypto";
import * as jwt from "jsonwebtoken";
import { newId } from "../common/ids/id.util";
import { PrismaService } from "../prisma/prisma.service";
import type { AdminUserPublicDto, AuthResponseDto } from "./dto/auth-response.dto";

const ACCESS_TTL_SECONDS = 15 * 60; // 15 min
const REFRESH_TTL_SECONDS = 30 * 24 * 60 * 60; // 30 días

export interface AdminAccessClaims {
  sub: string;
  email: string;
  role: AdminUserRole;
  kind: "admin_user_access";
}

interface AdminRefreshClaims {
  sub: string;
  jti: string;
  kind: "admin_user_refresh";
}

@Injectable()
export class AdminAuthService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly config: ConfigService,
  ) {}

  async login(email: string, password: string): Promise<AuthResponseDto> {
    const user = await this.prisma.adminUser.findUnique({ where: { email: email.toLowerCase() } });
    // Mismo mensaje para usuario inexistente y password inválido — no filtrar enumeración.
    if (!user || !user.active) throw new UnauthorizedException("Credenciales inválidas");
    const ok = await bcrypt.compare(password, user.passwordHash);
    if (!ok) throw new UnauthorizedException("Credenciales inválidas");

    await this.prisma.adminUser.update({
      where: { id: user.id },
      data: { lastLoginAt: new Date() },
    });

    return this.buildAuthResponse(user);
  }

  async refresh(refreshToken: string): Promise<AuthResponseDto> {
    let claims: AdminRefreshClaims;
    try {
      claims = jwt.verify(refreshToken, this.getRefreshSecret()) as AdminRefreshClaims;
    } catch {
      throw new UnauthorizedException("Refresh token inválido o expirado");
    }
    if (claims.kind !== "admin_user_refresh") {
      throw new UnauthorizedException("Refresh token inválido");
    }
    const user = await this.prisma.adminUser.findUnique({ where: { id: claims.sub } });
    if (!user || !user.active) throw new UnauthorizedException("Usuario no disponible");
    return this.buildAuthResponse(user);
  }

  async getMe(userId: string): Promise<AdminUserPublicDto> {
    const user = await this.prisma.adminUser.findUnique({ where: { id: userId } });
    if (!user || !user.active) throw new UnauthorizedException("Usuario no disponible");
    return this.toPublic(user);
  }

  verifyAccessToken(token: string): AdminAccessClaims {
    let claims: AdminAccessClaims;
    try {
      claims = jwt.verify(token, this.getAccessSecret()) as AdminAccessClaims;
    } catch {
      throw new UnauthorizedException("Token inválido o expirado");
    }
    if (claims.kind !== "admin_user_access") {
      throw new UnauthorizedException("Token inválido");
    }
    return claims;
  }

  /**
   * Bootstrap inicial: crea el primer superadmin usando el ADMIN_API_TOKEN como prueba
   * de posesión. Idempotente sobre email (devuelve 409 si ya existe).
   */
  async bootstrapFirstAdmin(input: { email: string; name: string; password: string }) {
    const email = input.email.toLowerCase();
    const existing = await this.prisma.adminUser.findUnique({ where: { email } });
    if (existing) {
      return { created: false, user: this.toPublic(existing) };
    }
    const passwordHash = await bcrypt.hash(input.password, 12);
    const user = await this.prisma.adminUser.create({
      data: {
        id: newId("adminUser"),
        email,
        name: input.name,
        passwordHash,
        role: AdminUserRole.superadmin,
        active: true,
      },
    });
    return { created: true, user: this.toPublic(user) };
  }

  private buildAuthResponse(user: AdminUser): AuthResponseDto {
    const accessClaims: AdminAccessClaims = {
      sub: user.id,
      email: user.email,
      role: user.role,
      kind: "admin_user_access",
    };
    const refreshClaims: AdminRefreshClaims = {
      sub: user.id,
      // jti único — abre la puerta a revocación por id en el futuro.
      jti: randomBytes(16).toString("hex"),
      kind: "admin_user_refresh",
    };

    const accessToken = jwt.sign(accessClaims, this.getAccessSecret(), {
      expiresIn: ACCESS_TTL_SECONDS,
      issuer: "novasis-pay",
      audience: "novasis-pay-admin",
    });
    const refreshToken = jwt.sign(refreshClaims, this.getRefreshSecret(), {
      expiresIn: REFRESH_TTL_SECONDS,
      issuer: "novasis-pay",
      audience: "novasis-pay-admin",
    });

    return {
      accessToken,
      refreshToken,
      expiresIn: ACCESS_TTL_SECONDS,
      user: this.toPublic(user),
    };
  }

  private toPublic(user: AdminUser): AdminUserPublicDto {
    return {
      id: user.id,
      email: user.email,
      name: user.name,
      role: user.role,
    };
  }

  private getAccessSecret(): string {
    const s = this.config.get<string>("ADMIN_JWT_SECRET");
    if (!s || s.length < 32) {
      throw new InternalServerErrorException(
        "ADMIN_JWT_SECRET no configurado (mínimo 32 caracteres)",
      );
    }
    return s;
  }

  private getRefreshSecret(): string {
    // Derivamos el refresh secret del access secret con un sufijo. Permite
    // rotar ambos cambiando una sola variable de entorno.
    return `${this.getAccessSecret()}::refresh`;
  }
}
