import {
  CanActivate,
  ExecutionContext,
  ForbiddenException,
  Inject,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { createHash } from 'node:crypto';
import { fromNodeHeaders } from 'better-auth/node';
import type { Auth } from '@mosaicstack/auth';
import type { Db } from '@mosaicstack/db';
import { eq, adminTokens, users as usersTable } from '@mosaicstack/db';
import type { FastifyRequest } from 'fastify';
import { AUTH } from '../auth/auth.tokens.js';
import { DB } from '../database/database.module.js';

interface UserWithRole {
  id: string;
  role?: string;
}

type AuthenticatedRequest = FastifyRequest & { user: unknown; session: unknown };

@Injectable()
export class AdminGuard implements CanActivate {
  constructor(
    @Inject(AUTH) private readonly auth: Auth,
    @Inject(DB) private readonly db: Db,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest<FastifyRequest>();

    // Try bearer token auth first
    const authHeader = request.raw.headers['authorization'];
    if (authHeader?.startsWith('Bearer ')) {
      return this.validateBearerToken(request, authHeader.slice(7));
    }

    // Fall back to BetterAuth session
    return this.validateSession(request);
  }

  private async validateBearerToken(request: FastifyRequest, plaintext: string): Promise<boolean> {
    const tokenHash = createHash('sha256').update(plaintext).digest('hex');

    const [row] = await this.db
      .select({
        tokenId: adminTokens.id,
        userId: adminTokens.userId,
        scope: adminTokens.scope,
        expiresAt: adminTokens.expiresAt,
        userName: usersTable.name,
        userEmail: usersTable.email,
        userRole: usersTable.role,
      })
      .from(adminTokens)
      .innerJoin(usersTable, eq(adminTokens.userId, usersTable.id))
      .where(eq(adminTokens.tokenHash, tokenHash))
      .limit(1);

    if (!row) {
      throw new UnauthorizedException('Invalid API token');
    }

    if (row.expiresAt && row.expiresAt < new Date()) {
      throw new UnauthorizedException('API token expired');
    }

    if (row.userRole !== 'admin') {
      throw new ForbiddenException('Admin access required');
    }

    // Update last-used timestamp (fire-and-forget)
    this.db
      .update(adminTokens)
      .set({ lastUsedAt: new Date() })
      .where(eq(adminTokens.id, row.tokenId))
      .then(() => {})
      .catch(() => {});

    const req = request as AuthenticatedRequest;
    req.user = { id: row.userId, name: row.userName, email: row.userEmail, role: row.userRole };
    req.session = { id: `token:${row.tokenId}`, userId: row.userId };

    return true;
  }

  private async validateSession(request: FastifyRequest): Promise<boolean> {
    const headers = fromNodeHeaders(request.raw.headers);
    const result = await this.auth.api.getSession({ headers });

    if (!result) {
      throw new UnauthorizedException('Invalid or expired session');
    }

    const user = result.user as UserWithRole;

    let userRole = user.role;
    if (!userRole) {
      const [dbUser] = await this.db
        .select({ role: usersTable.role })
        .from(usersTable)
        .where(eq(usersTable.id, user.id))
        .limit(1);
      userRole = dbUser?.role ?? 'member';
      (user as UserWithRole).role = userRole;
    }

    if (userRole !== 'admin') {
      throw new ForbiddenException('Admin access required');
    }

    const req = request as AuthenticatedRequest;
    req.user = result.user;
    req.session = result.session;

    return true;
  }
}
