import { Injectable, NotFoundException, UnauthorizedException, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcrypt';
import { PrismaService } from '../../prisma/prisma.service';

export interface UserRecord {
  id: string;
  clubId: string;
  phone: string;
  passwordHash: string;
  role: string;
  isActive: boolean;
  firstName: string;
  lastName: string;
  totpSecret: string | null;
}

export interface AuthUser {
  id: string;
  firstName: string;
  lastName: string;
  role: string;
  roles: string[];
  clubId: string;
}

export interface AuthTokens {
  accessToken: string;
  refreshToken: string;
  user: AuthUser;
}

export interface TokenPayload {
  sub: string;
  clubId: string;
  role: string;
  roles: string[];
}

@Injectable()
export class AuthService {
  private readonly logger = new Logger(AuthService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly jwtService: JwtService,
    private readonly configService: ConfigService,
  ) {}

  /**
   * Validate user credentials against the database.
   * Resets RLS context first since auth needs cross-tenant access.
   */
  async validateUser(phone: string, password: string): Promise<UserRecord> {
    await this.prisma.resetTenantId();

    const users = await this.prisma.$queryRawUnsafe<UserRecord[]>(
      `SELECT id, "clubId", phone, "passwordHash", role, "isActive", "firstName", "lastName", "totpSecret"
       FROM users
       WHERE phone = $1
       LIMIT 1`,
      phone,
    );

    if (!users.length) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const user = users[0];

    if (!user.isActive) {
      throw new UnauthorizedException('Account is deactivated');
    }

    const isPasswordValid = await bcrypt.compare(password, user.passwordHash);

    if (!isPasswordValid) {
      throw new UnauthorizedException('Invalid credentials');
    }

    return user;
  }

  /**
   * Fetch role slugs for a user from the user_roles join table.
   */
  private async getUserRoles(userId: string): Promise<string[]> {
    const rows = await this.prisma.$queryRawUnsafe<{ slug: string }[]>(
      `SELECT r.slug FROM user_roles ur JOIN roles r ON r.id = ur."roleId" WHERE ur."userId" = $1`,
      userId,
    );
    return rows.map((r) => r.slug);
  }

  /**
   * Determine primary role for backward compat: highest-privilege role.
   */
  private getPrimaryRole(roleSlugs: string[]): string {
    const priority = [
      'super_admin',
      'club_admin',
      'manager',
      'coordinator',
      'receptionist',
      'trainer',
    ];
    for (const p of priority) {
      if (roleSlugs.includes(p)) return p;
    }
    return roleSlugs[0] || 'trainer';
  }

  /**
   * Get user profile by ID.
   */
  async getProfile(userId: string): Promise<Omit<AuthUser, 'clubId'> & { phone: string }> {
    await this.prisma.resetTenantId();

    const users = await this.prisma.$queryRawUnsafe<UserRecord[]>(
      `SELECT id, "clubId", phone, role, "firstName", "lastName"
       FROM users WHERE id = $1 LIMIT 1`,
      userId,
    );

    if (!users.length) {
      throw new NotFoundException('User not found');
    }

    const u = users[0];
    const roleSlugs = await this.getUserRoles(u.id);
    const primaryRole = (
      roleSlugs.length > 0 ? this.getPrimaryRole(roleSlugs) : u.role
    ).toUpperCase();
    const roles = (roleSlugs.length > 0 ? roleSlugs : [u.role]).map((r) => r.toUpperCase());

    return {
      id: u.id,
      firstName: u.firstName,
      lastName: u.lastName,
      role: primaryRole,
      roles,
      phone: u.phone,
    };
  }

  /**
   * Generate JWT access + refresh tokens for the authenticated user.
   */
  async login(user: UserRecord): Promise<AuthTokens> {
    // Fetch roles from new table
    const roleSlugs = await this.getUserRoles(user.id);
    const primaryRole = (
      roleSlugs.length > 0 ? this.getPrimaryRole(roleSlugs) : user.role
    ).toUpperCase();
    const roles = (roleSlugs.length > 0 ? roleSlugs : [user.role]).map((r) => r.toUpperCase());

    const payload: TokenPayload = {
      sub: user.id,
      clubId: user.clubId,
      role: primaryRole,
      roles,
    };

    const refreshExpiresIn = this.configService.get<string>('jwt.refreshExpiresIn') || '30d';

    const [accessToken, refreshToken] = await Promise.all([
      this.jwtService.signAsync(payload),
      this.jwtService.signAsync(payload, {
        secret: this.configService.get<string>('jwt.refreshSecret'),
        expiresIn: refreshExpiresIn as `${number}${'s' | 'm' | 'h' | 'd'}`,
      }),
    ]);

    // Calculate expiration date for the refresh token (30 days default)
    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + 30);

    await this.storeRefreshToken(user.id, refreshToken, expiresAt);

    this.logger.log(`User ${user.id} logged in successfully`);

    return {
      accessToken,
      refreshToken,
      user: {
        id: user.id,
        firstName: user.firstName,
        lastName: user.lastName,
        role: primaryRole,
        roles,
        clubId: user.clubId,
      },
    };
  }

  /**
   * Validate refresh token and generate a new access token.
   * Resets RLS context since refresh_tokens is cross-tenant.
   */
  async refreshToken(token: string): Promise<{ accessToken: string }> {
    let payload: TokenPayload;

    try {
      payload = await this.jwtService.verifyAsync<TokenPayload>(token, {
        secret: this.configService.get<string>('jwt.refreshSecret'),
      });
    } catch {
      throw new UnauthorizedException('Invalid or expired refresh token');
    }

    await this.prisma.resetTenantId();

    const stored = await this.prisma.$queryRawUnsafe<{ id: string }[]>(
      `SELECT id FROM refresh_tokens
       WHERE "userId" = $1 AND token = $2 AND "expiresAt" > NOW()`,
      payload.sub,
      token,
    );

    if (!stored.length) {
      throw new UnauthorizedException('Refresh token has been revoked or expired');
    }

    // Re-fetch fresh roles on refresh
    const roleSlugs = await this.getUserRoles(payload.sub);
    const primaryRole = roleSlugs.length > 0 ? this.getPrimaryRole(roleSlugs) : payload.role;
    const roles = roleSlugs.length > 0 ? roleSlugs : payload.roles || [payload.role];

    const newPayload: TokenPayload = {
      sub: payload.sub,
      clubId: payload.clubId,
      role: primaryRole,
      roles,
    };

    const accessToken = await this.jwtService.signAsync(newPayload);

    return { accessToken };
  }

  /**
   * Store refresh token in the database for revocation tracking.
   * Uses ON CONFLICT to handle duplicate tokens (same user, same second).
   */
  private async storeRefreshToken(userId: string, token: string, expiresAt: Date): Promise<void> {
    await this.prisma.$executeRawUnsafe(
      `INSERT INTO refresh_tokens (id, "userId", token, "expiresAt", "createdAt")
       VALUES (gen_random_uuid(), $1, $2, $3::timestamp, NOW())
       ON CONFLICT (token) DO UPDATE SET "expiresAt" = EXCLUDED."expiresAt"`,
      userId,
      token,
      expiresAt.toISOString(),
    );
  }
}
