/* ============================================================
 * Copyright (c) 2026 RAVAPI Soluções. www.ravapi.com
 * Fase 2 — usuários, convites, papéis e permissão por série
 * ============================================================ */

import { randomBytes, createHash } from 'node:crypto';
import { emTransacao, type Pool } from '../db/client.ts';
import { exigir, type Papel, type UsuarioRef, type IdentityProvider } from '../core/identity.ts';
import { DadosInvalidos, NaoEncontrado, LimiteDoPlano } from '../core/errors.ts';

export const VALIDADE_CONVITE_DIAS = 7;

export interface NovoConvite {
  nome: string;
  email: string;
  papel: Papel;
  seriesPermitidas?: string[];
}

/** O token vai por e-mail; no banco fica só o hash. */
function gerarToken() {
  const token = randomBytes(32).toString('base64url');
  return { token, hash: createHash('sha256').update(token).digest('hex') };
}

export class UsuariosService {
  constructor(
    private readonly pool: Pool,
    private readonly limiteUsuarios: () => number | Promise<number>,
  ) {}

  async convidar(u: UsuarioRef, dados: NovoConvite): Promise<{ id: string; token: string }> {
    exigir(u, 'gerir_usuarios');

    const email = dados.email.trim().toLowerCase();
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email)) {
      throw new DadosInvalidos(`"${dados.email}" não é um e-mail válido.`);
    }
    if (dados.papel === 'AUDITOR' && dados.seriesPermitidas?.length) {
      throw new DadosInvalidos('Auditor não emite, então não faz sentido liberar séries para ele.');
    }

    const { token, hash } = gerarToken();

    return emTransacao(this.pool, async (x) => {
      // O convite pendente ocupa lugar no plano. Se não contasse, daria para
      // estourar o limite deixando convites abertos.
      const { rows: contagem } = await x.query(
        `SELECT count(*)::int AS n FROM recibos.users WHERE status IN ('ATIVO','CONVIDADO')`,
      );
      const limite = await this.limiteUsuarios();
      if (contagem[0].n >= limite) throw new LimiteDoPlano('usuários', limite);

      const { rows: existente } = await x.query(
        'SELECT id, status FROM recibos.users WHERE email = $1', [email],
      );
      if (existente[0]) {
        throw new DadosInvalidos(
          existente[0].status === 'CONVIDADO'
            ? `Já existe um convite pendente para ${email}. Reenvie em vez de criar outro.`
            : `${email} já é usuário desta empresa.`,
        );
      }

      const { rows } = await x.query(
        `INSERT INTO recibos.users
           (tenant_id, nome, email, papel, status, convidado_por, convidado_em,
            convite_token, convite_expira)
         VALUES (current_setting('app.tenant_id')::uuid, $1, $2, $3, 'CONVIDADO', $4, now(),
                 $5, now() + ($6 || ' days')::interval)
         RETURNING id`,
        [dados.nome.trim(), email, dados.papel, u.id, hash, String(VALIDADE_CONVITE_DIAS)],
      );

      for (const s of dados.seriesPermitidas ?? []) {
        await x.query(
          `INSERT INTO recibos.user_series_permissions (user_id, series_id, tenant_id)
           VALUES ($1, $2, current_setting('app.tenant_id')::uuid)`,
          [rows[0].id, s],
        );
      }
      return { id: rows[0].id, token };
    });
  }

  async aceitarConvite(token: string, senhaHash: string) {
    const hash = createHash('sha256').update(token).digest('hex');
    return emTransacao(this.pool, async (x) => {
      const { rows } = await x.query(
        `UPDATE recibos.users
            SET status = 'ATIVO', senha_hash = $2,
                convite_token = NULL, convite_expira = NULL
          WHERE convite_token = $1 AND status = 'CONVIDADO' AND convite_expira > now()
        RETURNING id, nome, email, papel`,
        [hash, senhaHash],
      );
      if (!rows[0]) {
        throw new DadosInvalidos(
          'Convite inválido, já usado ou vencido. Peça ao proprietário para reenviar.',
        );
      }
      return rows[0];
    });
  }

  /**
   * Desativar, nunca excluir: recibos selados apontam para o usuário e o
   * histórico precisa continuar legível. O banco recusa DELETE de qualquer jeito.
   */
  async desativar(u: UsuarioRef, usuarioId: string) {
    exigir(u, 'gerir_usuarios');
    if (u.id === usuarioId) {
      throw new DadosInvalidos('Você não pode desativar a própria conta.');
    }
    return emTransacao(this.pool, async (x) => {
      const { rows: alvo } = await x.query(
        'SELECT papel FROM recibos.users WHERE id = $1', [usuarioId],
      );
      if (!alvo[0]) throw new NaoEncontrado('Usuário');

      if (alvo[0].papel === 'PROPRIETARIO') {
        const { rows: restantes } = await x.query(
          `SELECT count(*)::int AS n FROM recibos.users
            WHERE papel = 'PROPRIETARIO' AND status = 'ATIVO' AND id <> $1`, [usuarioId],
        );
        if (restantes[0].n === 0) {
          throw new DadosInvalidos(
            'Este é o último proprietário ativo. Promova outra pessoa antes de desativá-lo, ' +
            'senão ninguém consegue mais administrar usuários, séries ou licença.',
          );
        }
      }
      const { rows } = await x.query(
        `UPDATE recibos.users SET status = 'INATIVO' WHERE id = $1 RETURNING id, nome, status`,
        [usuarioId],
      );
      return rows[0];
    });
  }

  async definirPermissoesDeSerie(u: UsuarioRef, usuarioId: string, seriesIds: string[]) {
    exigir(u, 'gerir_usuarios');
    return emTransacao(this.pool, async (x) => {
      await x.query('DELETE FROM recibos.user_series_permissions WHERE user_id = $1', [usuarioId]);
      for (const s of seriesIds) {
        await x.query(
          `INSERT INTO recibos.user_series_permissions (user_id, series_id, tenant_id)
           VALUES ($1, $2, current_setting('app.tenant_id')::uuid)`,
          [usuarioId, s],
        );
      }
      return { usuarioId, seriesIds };
    });
  }

  async listar(): Promise<UsuarioRef[]> {
    return emTransacao(this.pool, async (x) => {
      const { rows } = await x.query(
        `SELECT u.id, u.nome, u.email, u.papel, u.status,
                coalesce(array_agg(p.series_id) FILTER (WHERE p.series_id IS NOT NULL), '{}') AS series
           FROM recibos.users u
           LEFT JOIN recibos.user_series_permissions p ON p.user_id = u.id
          WHERE u.status <> 'INATIVO'
          GROUP BY u.id
          ORDER BY u.nome`,
      );
      return rows.map((r: any) => ({
        id: r.id, nome: r.nome, email: r.email, papel: r.papel,
        seriesPermitidas: r.series,
      }));
    });
  }
}

/** Modo SaaS: o módulo é dono dos usuários. */
export class IdentidadeLocal implements IdentityProvider {
  constructor(private readonly usuario: UsuarioRef, private readonly servico: UsuariosService) {}
  usuarioAtual() { return this.usuario; }
  listarMembros() { return this.servico.listar(); }
  administraUsuarios() { return true; }
}

/**
 * Modo embarcado: o sistema hospedeiro mapeia os próprios perfis para os
 * quatro papéis. As telas de gestão do módulo ficam ocultas — o cliente
 * administra usuários num lugar só.
 */
export class IdentidadeDelegada implements IdentityProvider {
  constructor(
    private readonly resolver: () => UsuarioRef,
    private readonly membros: () => Promise<UsuarioRef[]>,
  ) {}
  usuarioAtual() { return this.resolver(); }
  listarMembros() { return this.membros(); }
  administraUsuarios() { return false; }
}
