import { randomUUID } from 'node:crypto';

import {
  Inject,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import type { FastifyReply, FastifyRequest } from 'fastify';

import {
  SESSION_COOKIE_NAME,
  UserRole,
  createSessionExpiry,
  generateSecureToken,
  hashToken,
  type AuthenticatedUser,
  type SessionContext,
} from '@hexahost/auth';
import type { AppConfig } from '@hexahost/config';

import { APP_CONFIG } from '../config/config.constants';
import { PrismaService } from '../prisma/prisma.service';

@Injectable()
export class SessionService {
  constructor(
    private readonly prisma: PrismaService,
    @Inject(APP_CONFIG) private readonly config: AppConfig,
  ) {}

  async createSession(
    userId: string,
    ipAddress?: string,
    userAgent?: string,
  ): Promise<{ token: string; sessionId: string; expiresAt: Date }> {
    const token = generateSecureToken();
    const tokenHash = hashToken(token);
    const expiresAt = createSessionExpiry();

    const session = await this.prisma.userSession.create({
      data: {
        userId,
        tokenHash,
        expiresAt,
        ipAddress,
        userAgent,
      },
    });

    return { token, sessionId: session.id, expiresAt };
  }

  setSessionCookie(reply: FastifyReply, token: string, expiresAt: Date): void {
    const isProduction = this.config.APP_ENV === 'production';

    void reply.setCookie(SESSION_COOKIE_NAME, token, {
      httpOnly: true,
      secure: isProduction,
      sameSite: 'lax',
      path: '/',
      expires: expiresAt,
    });
  }

  clearSessionCookie(reply: FastifyReply): void {
    void reply.clearCookie(SESSION_COOKIE_NAME, {
      path: '/',
    });
  }

  readSessionToken(request: FastifyRequest): string | undefined {
    const cookies = request.cookies as Record<string, string | undefined>;
    const token = cookies[SESSION_COOKIE_NAME];
    return token && token.length > 0 ? token : undefined;
  }

  async validateSessionToken(token: string): Promise<SessionContext> {
    const tokenHash = hashToken(token);
    const now = new Date();

    const session = await this.prisma.userSession.findUnique({
      where: { tokenHash },
      include: {
        user: {
          include: {
            platformRoles: {
              include: {
                role: true,
              },
            },
          },
        },
      },
    });

    if (
      !session ||
      session.revokedAt ||
      session.expiresAt <= now ||
      session.user.status !== 'ACTIVE'
    ) {
      throw new UnauthorizedException('Invalid or expired session');
    }

    return {
      sessionId: session.id,
      issuedAt: session.createdAt,
      expiresAt: session.expiresAt,
      user: this.toAuthenticatedUser(session.user),
    };
  }

  async revokeSession(sessionId: string, userId: string): Promise<void> {
    const session = await this.prisma.userSession.findFirst({
      where: { id: sessionId, userId },
    });

    if (!session || session.revokedAt) {
      return;
    }

    await this.prisma.userSession.update({
      where: { id: sessionId },
      data: { revokedAt: new Date() },
    });
  }

  async revokeByToken(token: string): Promise<string | undefined> {
    const tokenHash = hashToken(token);
    const session = await this.prisma.userSession.findUnique({
      where: { tokenHash },
    });

    if (!session || session.revokedAt) {
      return undefined;
    }

    await this.prisma.userSession.update({
      where: { id: session.id },
      data: { revokedAt: new Date() },
    });

    return session.userId;
  }

  async listSessions(userId: string, currentSessionId: string) {
    const sessions = await this.prisma.userSession.findMany({
      where: {
        userId,
        revokedAt: null,
        expiresAt: { gt: new Date() },
      },
      orderBy: { createdAt: 'desc' },
    });

    return sessions.map((session) => ({
      id: session.id,
      ipAddress: session.ipAddress,
      userAgent: session.userAgent,
      createdAt: session.createdAt.toISOString(),
      expiresAt: session.expiresAt.toISOString(),
      current: session.id === currentSessionId,
    }));
  }

  createLoginChallengeId(): string {
    return randomUUID();
  }

  toAuthenticatedUser(
    user: {
      id: string;
      email: string;
      username: string;
      displayName: string | null;
      emailVerifiedAt: Date | null;
      platformRoles: Array<{ role: { name: string } }>;
    },
  ): AuthenticatedUser {
    const roles = user.platformRoles
      .map((entry) => entry.role.name)
      .filter((name): name is UserRole =>
        Object.values(UserRole).includes(name as UserRole),
      );

    return {
      id: user.id,
      email: user.email,
      username: user.username,
      displayName: user.displayName,
      emailVerified: user.emailVerifiedAt !== null,
      roles: roles.length > 0 ? roles : [UserRole.USER],
    };
  }
}
