import { createHash, randomBytes } from 'node:crypto'
import jwt from 'jsonwebtoken'
import type { ITokenService, AccessTokenPayload } from '../../domain/ports/token-service'
import { isUserRole } from '../../domain/entities/user'
import { AppError } from '../../domain/errors/app-error'
import type { Env } from '../config/env'

function parseDurationMs(duration: string): number {
  const match = /^(\d+)([smhd])$/.exec(duration)
  if (!match || !match[1] || !match[2]) {
    throw new Error(`Duración inválida: "${duration}". Formato esperado: 15m, 1h, 7d`)
  }
  const value = parseInt(match[1], 10)
  const unit = match[2]
  const multipliers: Record<string, number> = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }
  const ms = multipliers[unit]
  if (ms === undefined) throw new Error(`Unidad desconocida: "${unit}"`)
  return value * ms
}

export class JwtService implements ITokenService {
  private readonly secret: string
  private readonly accessExpiresSecs: number
  private readonly refreshTtlMsValue: number

  constructor(env: Pick<Env, 'JWT_SECRET' | 'JWT_ACCESS_EXPIRES' | 'JWT_REFRESH_EXPIRES'>) {
    this.secret = env.JWT_SECRET
    this.accessExpiresSecs = Math.floor(parseDurationMs(env.JWT_ACCESS_EXPIRES) / 1000)
    this.refreshTtlMsValue = parseDurationMs(env.JWT_REFRESH_EXPIRES)
  }

  generateAccessToken(payload: AccessTokenPayload): string {
    // Se pone exp en el payload directamente para evitar incompatibilidades
    // de tipo con StringValue de exactOptionalPropertyTypes
    const now = Math.floor(Date.now() / 1000)
    return jwt.sign(
      { sub: payload.userId, role: payload.role, exp: now + this.accessExpiresSecs },
      this.secret,
    )
  }

  verifyAccessToken(token: string): AccessTokenPayload {
    let decoded: string | jwt.JwtPayload
    try {
      decoded = jwt.verify(token, this.secret)
    } catch {
      throw new AppError('INVALID_TOKEN', 401, 'Token inválido o expirado')
    }

    if (typeof decoded === 'string' || !decoded.sub) {
      throw new AppError('INVALID_TOKEN', 401, 'Token inválido')
    }

    const rawRole: unknown = decoded['role']
    if (!isUserRole(rawRole)) {
      throw new AppError('INVALID_TOKEN', 401, 'Token inválido')
    }

    return { userId: decoded.sub, role: rawRole }
  }

  generateOpaqueToken(): string {
    return randomBytes(48).toString('hex')
  }

  hashToken(token: string): string {
    return createHash('sha256').update(token).digest('hex')
  }

  refreshTokenTtlMs(): number {
    return this.refreshTtlMsValue
  }
}
