import { randomUUID } from 'node:crypto';

import type { Pool, PoolClient, QueryResultRow } from 'pg';

import { db } from '../db.js';
import { verifyPassword } from './password.js';
import {
  generateRefreshToken,
  getAuthTokenConfig,
  hashRefreshToken,
  issueAccessToken,
  verifyAccessToken,
} from './token.js';

type AuthStatus = 400 | 401 | 403 | 404 | 409 | 500;

export class AuthError extends Error {
  constructor(
    message: string,
    readonly status: AuthStatus,
    readonly code: string,
  ) {
    super(message);
    this.name = 'AuthError';
  }
}

interface AuthUserRow extends QueryResultRow {
  id: string;
  email: string | null;
  encrypted_password: string | null;
  role: string | null;
  confirmed_at: Date | null;
  email_confirmed_at: Date | null;
  banned_until: Date | null;
  deleted_at: Date | null;
  is_anonymous: boolean;
  raw_user_meta_data: unknown;
}

interface RefreshRow extends AuthUserRow {
  refresh_token_id: string;
  session_id: string;
  refresh_revoked: boolean;
  refresh_used_at: Date | null;
  refresh_expires_at: Date | null;
  session_revoked_at: Date | null;
  session_not_after: Date | null;
}

interface ProfileRow extends QueryResultRow {
  key: string;
  value: unknown;
}

interface CompanyRow extends QueryResultRow {
  value: unknown;
}

export interface ErpProfile extends Record<string, unknown> {
  id?: string;
  authUserId: string;
  email?: string;
  nome?: string;
  empresaId?: string;
  nivel?: string;
  tipo?: string;
  ativo?: boolean;
  permissoes?: Record<string, unknown>;
  modulosPermitidos?: string[];
}

export interface AuthenticatedUser {
  id: string;
  email: string;
  role: string;
  metadata: Record<string, unknown>;
  profile: ErpProfile;
  companyName: string | null;
}

export interface IssuedSession {
  accessToken: string;
  refreshToken: string;
  tokenType: 'Bearer';
  expiresIn: number;
  expiresAt: string;
  refreshExpiresAt: string;
  sessionId: string;
  user: AuthenticatedUser;
}

export interface RequestContext {
  userAgent?: string | null | undefined;
}

type Queryable = Pick<Pool, 'query'> | Pick<PoolClient, 'query'>;

function normalizeEmail(value: unknown): string | null {
  if (typeof value !== 'string') {
    return null;
  }

  const email = value.trim().toLowerCase();
  if (
    email.length < 3 ||
    email.length > 320 ||
    !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
  ) {
    return null;
  }

  return email;
}

function asRecord(value: unknown): Record<string, unknown> {
  return value !== null && typeof value === 'object' && !Array.isArray(value)
    ? (value as Record<string, unknown>)
    : {};
}

function isInactive(profile: ErpProfile): boolean {
  const active = (profile as Record<string, unknown>).ativo;
  return active === false || active === 'false' || active === 0;
}

function isUnavailable(user: AuthUserRow, now: Date): boolean {
  return (
    user.deleted_at !== null ||
    user.is_anonymous ||
    (user.banned_until !== null && user.banned_until.getTime() > now.getTime())
  );
}

function isConfirmed(user: AuthUserRow): boolean {
  return user.confirmed_at !== null || user.email_confirmed_at !== null;
}

function safeUserAgent(value: string | null | undefined): string | null {
  if (!value) {
    return null;
  }

  const sanitized = value.replace(/[\u0000-\u001f\u007f]/g, '').trim();
  return sanitized ? sanitized.slice(0, 512) : null;
}

function dateIsPast(value: Date | null, now: Date): boolean {
  return value === null || value.getTime() <= now.getTime();
}

async function inTransaction<T>(
  pool: Pool,
  work: (client: PoolClient) => Promise<T>,
): Promise<T> {
  const client = await pool.connect();

  try {
    await client.query('BEGIN');
    const result = await work(client);
    await client.query('COMMIT');
    return result;
  } catch (error) {
    try {
      await client.query('ROLLBACK');
    } catch {
      // Preserve the original failure; the pool will discard a broken client.
    }
    throw error;
  } finally {
    client.release();
  }
}

export class AuthService {
  constructor(private readonly pool: Pool = db) {}

  async signIn(
    emailInput: unknown,
    password: unknown,
    request: RequestContext = {},
  ): Promise<IssuedSession> {
    const email = normalizeEmail(emailInput);
    if (!email || typeof password !== 'string') {
      throw new AuthError('Email e senha são obrigatórios.', 400, 'invalid_request');
    }

    // Validate all token configuration before accepting the password or
    // creating a database session.
    const tokenConfig = getAuthTokenConfig();
    const userResult = await this.pool.query<AuthUserRow>(
      `SELECT id, email, encrypted_password, role, confirmed_at,
              email_confirmed_at, banned_until, deleted_at, is_anonymous,
              raw_user_meta_data
       FROM app_auth.users
       WHERE lower(email) = $1
         AND deleted_at IS NULL
       ORDER BY
         CASE WHEN confirmed_at IS NOT NULL OR email_confirmed_at IS NOT NULL
           THEN 0 ELSE 1 END,
         updated_at DESC,
         id
       LIMIT 1`,
      [email],
    );
    const user = userResult.rows[0];
    const passwordMatches = await verifyPassword(password, user?.encrypted_password);

    if (!user || !passwordMatches) {
      throw new AuthError(
        'Email ou senha incorretos. Verifique suas credenciais e tente novamente.',
        401,
        'invalid_credentials',
      );
    }

    const now = new Date();
    if (isUnavailable(user, now)) {
      throw new AuthError('Esta conta não está disponível.', 403, 'account_unavailable');
    }
    if (!isConfirmed(user)) {
      throw new AuthError(
        'Email não confirmado. Entre em contacto com o administrador.',
        403,
        'email_not_confirmed',
      );
    }

    const authenticatedUser = await this.loadAuthenticatedUser(this.pool, user);
    const sessionId = randomUUID();
    const refreshToken = generateRefreshToken();
    const refreshTokenHash = hashRefreshToken(refreshToken);
    const refreshExpiresAt = new Date(
      now.getTime() + tokenConfig.refreshTokenTtlSeconds * 1_000,
    );
    const access = await issueAccessToken({
      userId: user.id,
      sessionId,
      email: authenticatedUser.email,
      role: authenticatedUser.role,
      now,
    });

    await inTransaction(this.pool, async (client) => {
      await client.query(
        `INSERT INTO app_auth.sessions
           (id, user_id, created_at, updated_at, aal, not_after,
            refreshed_at, user_agent, tag, source_record)
         VALUES
           ($1, $2, $3, $3, 'aal1', $4, $3::timestamptz AT TIME ZONE 'UTC',
            $5, 'local-auth', '{"issued_by":"local-auth"}'::jsonb)`,
        [sessionId, user.id, now, refreshExpiresAt, safeUserAgent(request.userAgent)],
      );
      await client.query(
        `INSERT INTO app_auth.refresh_tokens
           (user_id, session_id, token, token_hash, revoked, expires_at,
            created_at, updated_at, source_record)
         VALUES
           ($1, $2, NULL, $3, false, $4, $5, $5,
            '{"issued_by":"local-auth"}'::jsonb)`,
        [user.id, sessionId, refreshTokenHash, refreshExpiresAt, now],
      );
      await client.query(
        `UPDATE app_auth.users
         SET last_sign_in_at = $2
         WHERE id = $1`,
        [user.id, now],
      );
    });

    return {
      accessToken: access.token,
      refreshToken,
      tokenType: 'Bearer',
      expiresIn: access.expiresIn,
      expiresAt: access.expiresAt.toISOString(),
      refreshExpiresAt: refreshExpiresAt.toISOString(),
      sessionId,
      user: authenticatedUser,
    };
  }

  async refresh(refreshToken: unknown, request: RequestContext = {}): Promise<IssuedSession> {
    const tokenConfig = getAuthTokenConfig();
    let refreshTokenHash: string;

    try {
      refreshTokenHash = hashRefreshToken(refreshToken);
    } catch {
      throw new AuthError('Sessão inválida ou expirada.', 401, 'invalid_refresh_token');
    }

    const now = new Date();
    const nextRefreshToken = generateRefreshToken();
    const nextRefreshTokenHash = hashRefreshToken(nextRefreshToken);
    const nextRefreshExpiresAt = new Date(
      now.getTime() + tokenConfig.refreshTokenTtlSeconds * 1_000,
    );

    const outcome = await inTransaction(this.pool, async (client) => {
      const result = await client.query<RefreshRow>(
        `SELECT rt.id::text AS refresh_token_id, rt.session_id,
                rt.revoked AS refresh_revoked,
                rt.used_at AS refresh_used_at,
                rt.expires_at AS refresh_expires_at,
                s.revoked_at AS session_revoked_at,
                s.not_after AS session_not_after,
                u.id, u.email, u.encrypted_password, u.role, u.confirmed_at,
                u.email_confirmed_at, u.banned_until, u.deleted_at,
                u.is_anonymous, u.raw_user_meta_data
         FROM app_auth.refresh_tokens AS rt
         JOIN app_auth.sessions AS s ON s.id = rt.session_id
         JOIN app_auth.users AS u ON u.id = rt.user_id
         WHERE rt.token_hash = $1
         FOR UPDATE OF rt, s`,
        [refreshTokenHash],
      );
      const row = result.rows[0];

      if (!row) {
        return { kind: 'invalid' as const };
      }

      if (row.refresh_revoked || row.refresh_used_at !== null) {
        await this.revokeSession(client, row.session_id, now);
        return { kind: 'replayed' as const };
      }

      if (
        dateIsPast(row.refresh_expires_at, now) ||
        row.session_revoked_at !== null ||
        dateIsPast(row.session_not_after, now) ||
        isUnavailable(row, now) ||
        !isConfirmed(row)
      ) {
        await this.revokeSession(client, row.session_id, now);
        return { kind: 'invalid' as const };
      }

      const authenticatedUser = await this.loadAuthenticatedUser(client, row);

      await client.query(
        `UPDATE app_auth.refresh_tokens
         SET revoked = true, revoked_at = $2, used_at = $2
         WHERE id = $1`,
        [row.refresh_token_id, now],
      );
      await client.query(
        `INSERT INTO app_auth.refresh_tokens
           (user_id, session_id, token, token_hash, parent, revoked,
            expires_at, created_at, updated_at, source_record)
         VALUES
           ($1, $2, NULL, $3, $4, false, $5, $6, $6,
            '{"issued_by":"local-auth"}'::jsonb)`,
        [
          row.id,
          row.session_id,
          nextRefreshTokenHash,
          refreshTokenHash,
          nextRefreshExpiresAt,
          now,
        ],
      );
      await client.query(
        `UPDATE app_auth.sessions
         SET refreshed_at = $2::timestamptz AT TIME ZONE 'UTC',
             not_after = $3,
             user_agent = COALESCE($4, user_agent)
         WHERE id = $1`,
        [
          row.session_id,
          now,
          nextRefreshExpiresAt,
          safeUserAgent(request.userAgent),
        ],
      );

      return {
        kind: 'success' as const,
        sessionId: row.session_id,
        user: authenticatedUser,
      };
    });

    if (outcome.kind !== 'success') {
      throw new AuthError(
        'Sessão inválida ou expirada.',
        401,
        outcome.kind === 'replayed' ? 'refresh_token_reused' : 'invalid_refresh_token',
      );
    }

    const access = await issueAccessToken({
      userId: outcome.user.id,
      sessionId: outcome.sessionId,
      email: outcome.user.email,
      role: outcome.user.role,
      now,
    });

    return {
      accessToken: access.token,
      refreshToken: nextRefreshToken,
      tokenType: 'Bearer',
      expiresIn: access.expiresIn,
      expiresAt: access.expiresAt.toISOString(),
      refreshExpiresAt: nextRefreshExpiresAt.toISOString(),
      sessionId: outcome.sessionId,
      user: outcome.user,
    };
  }

  async getUser(accessToken: unknown): Promise<AuthenticatedUser> {
    let claims;
    try {
      claims = await verifyAccessToken(accessToken);
    } catch {
      throw new AuthError('Sessão inválida ou expirada.', 401, 'invalid_access_token');
    }

    const result = await this.pool.query<AuthUserRow>(
      `SELECT u.id, u.email, u.encrypted_password, u.role, u.confirmed_at,
              u.email_confirmed_at, u.banned_until, u.deleted_at,
              u.is_anonymous, u.raw_user_meta_data
       FROM app_auth.sessions AS s
       JOIN app_auth.users AS u ON u.id = s.user_id
       WHERE s.id = $1
         AND s.user_id = $2
         AND s.revoked_at IS NULL
         AND (s.not_after IS NULL OR s.not_after > transaction_timestamp())
         AND u.deleted_at IS NULL`,
      [claims.sessionId, claims.userId],
    );
    const user = result.rows[0];

    if (!user || isUnavailable(user, new Date()) || !isConfirmed(user)) {
      throw new AuthError('Sessão inválida ou expirada.', 401, 'invalid_session');
    }

    return this.loadAuthenticatedUser(this.pool, user);
  }

  async signOut(accessToken: unknown, refreshToken?: unknown): Promise<void> {
    let sessionId: string | null = null;
    let userId: string | null = null;

    if (typeof accessToken === 'string' && accessToken !== '') {
      try {
        const claims = await verifyAccessToken(accessToken);
        sessionId = claims.sessionId;
        userId = claims.userId;
      } catch {
        // An expired access token may still be paired with a valid refresh
        // token, which remains sufficient to locate and revoke the session.
      }
    }

    let refreshTokenHash: string | null = null;
    if (refreshToken !== undefined) {
      try {
        refreshTokenHash = hashRefreshToken(refreshToken);
      } catch {
        // Keep the public response independent of refresh token shape.
      }
    }

    if (!sessionId && !refreshTokenHash) {
      throw new AuthError('Token de acesso não fornecido.', 401, 'token_required');
    }

    const now = new Date();
    await inTransaction(this.pool, async (client) => {
      if (!sessionId && refreshTokenHash) {
        const result = await client.query<{ session_id: string; user_id: string }>(
          `SELECT session_id, user_id
           FROM app_auth.refresh_tokens
           WHERE token_hash = $1
           FOR UPDATE`,
          [refreshTokenHash],
        );
        sessionId = result.rows[0]?.session_id ?? null;
        userId = result.rows[0]?.user_id ?? null;
      }

      if (sessionId && userId) {
        await client.query(
          `UPDATE app_auth.sessions
           SET revoked_at = COALESCE(revoked_at, $3)
           WHERE id = $1 AND user_id = $2`,
          [sessionId, userId, now],
        );
        await client.query(
          `UPDATE app_auth.refresh_tokens
           SET revoked = true, revoked_at = COALESCE(revoked_at, $3)
           WHERE session_id = $1 AND user_id = $2 AND revoked = false`,
          [sessionId, userId, now],
        );
      }
    });
  }

  private async revokeSession(
    client: PoolClient,
    sessionId: string,
    now: Date,
  ): Promise<void> {
    await client.query(
      `UPDATE app_auth.sessions
       SET revoked_at = COALESCE(revoked_at, $2)
       WHERE id = $1`,
      [sessionId, now],
    );
    await client.query(
      `UPDATE app_auth.refresh_tokens
       SET revoked = true, revoked_at = COALESCE(revoked_at, $2)
       WHERE session_id = $1 AND revoked = false`,
      [sessionId, now],
    );
  }

  private async loadAuthenticatedUser(
    queryable: Queryable,
    user: AuthUserRow,
  ): Promise<AuthenticatedUser> {
    if (!user.email) {
      throw new AuthError('Esta conta não possui email válido.', 403, 'email_missing');
    }

    const profileResult = await queryable.query<ProfileRow>(
      `SELECT key, value
       FROM public.kv_store_7249dcd9
       WHERE (key LIKE 'usuario:%' OR key LIKE 'revendedor:%')
         AND jsonb_typeof(value) = 'object'
         AND value ->> 'authUserId' = $1
       ORDER BY
         CASE WHEN key = 'usuario:' || $1 THEN 0 ELSE 1 END,
         CASE WHEN key = 'usuario:' || COALESCE(value ->> 'id', '') THEN 0 ELSE 1 END,
         CASE WHEN key LIKE 'usuario:%' THEN 0 ELSE 1 END,
         key`,
      [user.id],
    );
    const selected = profileResult.rows[0];

    if (!selected) {
      throw new AuthError(
        'Utilizador não encontrado no sistema. Contacte o administrador.',
        403,
        'erp_profile_not_found',
      );
    }

    const profile = asRecord(selected.value) as ErpProfile;
    if (isInactive(profile)) {
      throw new AuthError('Esta conta está desactivada.', 403, 'erp_profile_inactive');
    }

    const companyName = await this.loadCompanyName(queryable, profile.empresaId);
    const level = typeof profile.nivel === 'string' ? profile.nivel : undefined;
    if (profile.empresaId && companyName === null && level !== 'super_admin') {
      throw new AuthError(
        'A empresa associada a esta conta já não existe. Contacte o administrador.',
        403,
        'company_not_found',
      );
    }

    return {
      id: user.id,
      email: user.email,
      role: user.role || 'authenticated',
      metadata: asRecord(user.raw_user_meta_data),
      profile,
      companyName,
    };
  }

  private async loadCompanyName(
    queryable: Queryable,
    companyId: unknown,
  ): Promise<string | null> {
    if (typeof companyId !== 'string' || companyId.length === 0) {
      return null;
    }

    const result = await queryable.query<CompanyRow>(
      `SELECT value
       FROM public.kv_store_7249dcd9
       WHERE key = $1`,
      [`empresa:${companyId}`],
    );
    const company = asRecord(result.rows[0]?.value);
    const name = company.nomeComercial ?? company.nome;
    return typeof name === 'string' && name.trim() ? name : null;
  }
}

export const authService = new AuthService();
