import { BadRequestException, ConflictException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { AccountStatus, EstateStatus, Prisma, UserRole } from '@prisma/client';
import * as bcrypt from 'bcrypt';
import { AuditService } from '../audit/audit.service';
import { generateCode } from '../common/code-generator';
import { calculateProfileCompletion } from '../common/profile-completion';
import { getNumericPlanLimit, PlanLimitKey } from '../common/plan-limits';
import { resolveEstateScope } from '../common/estate-scope';
import { RequestUser } from '../common/request-user';
import { PrismaService } from '../database/prisma.service';
import { LoginDto, RefreshTokenDto, SignupDto, VerifyOtpDto } from './dto/auth.dto';
import { OtpService } from './otp.service';

@Injectable()
export class AuthService {
  private readonly BCRYPT_ROUNDS = 10;

  constructor(
    private readonly prisma: PrismaService,
    private readonly jwt: JwtService,
    private readonly config: ConfigService,
    private readonly audit: AuditService,
    private readonly otp: OtpService
  ) {}

  async login(dto: LoginDto) {
    const user = await this.getLoginUser(dto.phone);
    return this.otp.sendOtp(dto.phone, user.name);
  }

  async requestConsoleOtp(phone: string) {
    const user = await this.getLoginUser(phone);
    if (user.role !== UserRole.SUPER_ADMIN) {
      throw new UnauthorizedException('Only super admins can access the API console');
    }
    return this.otp.sendOtp(phone, user.name);
  }

  async verifyOtp(dto: VerifyOtpDto) {
    const user = await this.getLoginUser(dto.phone);
    if (!this.otp.verifyOtp(dto.phone, dto.otp)) {
      throw new UnauthorizedException('Invalid OTP');
    }

    await this.prisma.user.update({
      where: { id: user.id },
      data: { phoneVerifiedAt: user.phoneVerifiedAt ?? new Date() }
    });

    return this.issueTokens(user.id, user.estateId, user.role);
  }

  async issueConsoleSession(dto: VerifyOtpDto) {
    const user = await this.getLoginUser(dto.phone);
    if (user.role !== UserRole.SUPER_ADMIN) {
      throw new UnauthorizedException('Only super admins can access the API console');
    }
    if (!this.otp.verifyOtp(dto.phone, dto.otp)) {
      throw new UnauthorizedException('Invalid OTP');
    }

    return this.jwt.signAsync(
      { sub: user.id, estateId: user.estateId, role: user.role, type: 'console' },
      {
        secret: this.config.get('JWT_ACCESS_SECRET'),
        expiresIn: this.config.get<string>('CONSOLE_SESSION_TTL', '8h') as '8h'
      }
    );
  }

  async refresh(dto: RefreshTokenDto) {
    try {
      // First verify JWT signature
      const payload = await this.jwt.verifyAsync<{ sub: string; estateId: string | null; role: string }>(
        dto.refreshToken,
        { secret: this.config.get('JWT_REFRESH_SECRET') }
      );

      // Then verify token is still in DB by comparing hash
      const user = await this.prisma.user.findUnique({
        where: { id: payload.sub },
        include: { estate: { select: { status: true } } }
      });
      if (!user || !user.refreshToken) {
        throw new UnauthorizedException('Refresh token not found');
      }
      this.assertActiveAccount(user.status, user.role, user.estate?.status);

      // Verify the token hash matches
      const tokenIsValid = await bcrypt.compare(dto.refreshToken, user.refreshToken);
      if (!tokenIsValid) {
        throw new UnauthorizedException('Refresh token is invalid or revoked');
      }

      return this.issueTokens(payload.sub, payload.estateId, payload.role);
    } catch (error) {
      if (error instanceof UnauthorizedException) {
        throw error;
      }
      throw new UnauthorizedException('Invalid refresh token');
    }
  }

  async signup(dto: SignupDto, actor: RequestUser) {
    this.assertCanCreateRole(actor, dto.role);
    const estateId = this.resolveSignupEstate(actor, dto);
    this.assertRoleSpecificFields(dto);
    await this.assertPlanRoleLimit(estateId, dto.role);

    try {
      const result = await this.prisma.$transaction(async (tx) => {
        const account = await tx.user.create({
          data: {
            estateId,
            name: dto.name,
            phone: dto.phone,
            firstName: dto.firstName,
            lastName: dto.lastName,
            email: dto.email,
            address: dto.address,
            role: dto.role
          }
        });

        if (dto.role === UserRole.RESIDENT) {
          const draft = {
            estateId: estateId ?? this.requiredEstateIdForRole(dto.role),
            firstName: dto.firstName,
            lastName: dto.lastName,
            phone: dto.phone,
            blockNumber: dto.blockNumber,
            flatNumber: dto.flatNumber ?? dto.unit,
            unit: dto.flatNumber ?? dto.unit,
            address: dto.address,
            profileConfirmed: false
          };
          const completion = calculateProfileCompletion(UserRole.RESIDENT, draft);
          const resident = await tx.resident.create({
            data: {
              estateId: estateId ?? this.requiredEstateIdForRole(dto.role),
              userId: account.id,
            residentCode: `RES-${generateCode()}`,
              name: dto.name,
              firstName: dto.firstName,
              lastName: dto.lastName,
              phone: dto.phone,
              unit: dto.flatNumber ?? dto.unit ?? '',
              blockNumber: dto.blockNumber,
              flatNumber: dto.flatNumber ?? dto.unit,
              address: dto.address,
              email: dto.email,
              profileCompletion: completion.completion
            }
          });
          return { user: account, resident };
        }

        if (dto.role === UserRole.GUARD) {
          const draft = {
            estateId: estateId ?? this.requiredEstateIdForRole(dto.role),
            firstName: dto.firstName,
            lastName: dto.lastName,
            phone: dto.phone,
            guardCode: `GRD-${generateCode()}`,
            assignedGateId: dto.assignedGateId,
            address: dto.address,
            profileConfirmed: false
          };
          const completion = calculateProfileCompletion(UserRole.GUARD, draft);
          const guard = await tx.guard.create({
            data: {
              estateId: estateId ?? this.requiredEstateIdForRole(dto.role),
              userId: account.id,
              guardCode: draft.guardCode,
              name: dto.name,
              firstName: dto.firstName,
              lastName: dto.lastName,
              phone: dto.phone,
              address: dto.address,
              assignedGateId: dto.assignedGateId,
              profileCompletion: completion.completion,
              deviceId: dto.deviceId
            }
          });
          return { user: account, guard };
        }

        return { user: account };
      });

      await this.audit.record({
        estateId,
        actorType: 'USER',
        actorId: actor.id,
        action: 'USER_SIGNED_UP',
        metadata: {
          userId: result.user.id,
          role: result.user.role,
          phone: result.user.phone
        }
      });

      return result;
    } catch (error) {
      if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
        throw new ConflictException('A user with this phone number already exists');
      }
      throw error;
    }
  }

  private async issueTokens(userId: string, estateId: string | null, role: string) {
    const payload = { sub: userId, estateId, role };
    const accessToken = await this.jwt.signAsync(payload, {
      secret: this.config.get('JWT_ACCESS_SECRET'),
      expiresIn: '15m'
    });
    const refreshToken = await this.jwt.signAsync(payload, {
      secret: this.config.get('JWT_REFRESH_SECRET'),
      expiresIn: '30d'
    });

    // Hash refresh token before storing for security
    const hashedRefreshToken = await bcrypt.hash(refreshToken, this.BCRYPT_ROUNDS);

    await this.prisma.user.update({
      where: { id: userId },
      data: { refreshToken: hashedRefreshToken }
    });

    const sessionUser = await this.prisma.user.findUnique({
      where: { id: userId },
      select: {
        id: true,
        name: true,
        firstName: true,
        lastName: true,
        phone: true,
        role: true,
        estateId: true,
        estate: {
          select: {
            id: true,
            name: true,
            location: true,
            subscriptionPlan: { select: { code: true, name: true, sortOrder: true } }
          }
        }
      }
    });

    return {
      accessToken,
      refreshToken,
      user: sessionUser
        ? {
            id: sessionUser.id,
            name: sessionUser.name,
            firstName: sessionUser.firstName,
            lastName: sessionUser.lastName,
            phone: sessionUser.phone,
            role: sessionUser.role,
            estateId: sessionUser.estateId
          }
        : null,
      estate: sessionUser?.estate ?? null,
      estateName: sessionUser?.estate?.name ?? null,
      estatePlanCode: sessionUser?.estate?.subscriptionPlan?.code ?? null
    };
  }

  private async getLoginUser(phone: string) {
    const user = await this.prisma.user.findUnique({
      where: { phone },
      include: { estate: { select: { status: true } } }
    });
    if (!user) {
      throw new UnauthorizedException('No account exists for this phone number');
    }
    this.assertActiveAccount(user.status, user.role, user.estate?.status);
    return user;
  }

  private assertActiveAccount(status: AccountStatus, role: UserRole | string, estateStatus?: EstateStatus) {
    if (status !== AccountStatus.ACTIVE) {
      throw new UnauthorizedException('Account is not active');
    }
    if (!this.isPlatformRole(role) && estateStatus !== EstateStatus.ACTIVE) {
      throw new UnauthorizedException('Estate is not active');
    }
  }

  private assertCanCreateRole(actor: RequestUser, role: UserRole) {
    if (actor.role === UserRole.SUPER_ADMIN) {
      return;
    }

    const estateAdminRoles: UserRole[] = [UserRole.SECURITY_SUPERVISOR, UserRole.GUARD, UserRole.RESIDENT];
    const supervisorRoles: UserRole[] = [UserRole.GUARD, UserRole.RESIDENT];
    const allowedRoles =
      actor.role === UserRole.ESTATE_ADMIN
        ? estateAdminRoles
        : actor.role === UserRole.SECURITY_SUPERVISOR
          ? supervisorRoles
          : [];

    if (!allowedRoles.includes(role)) {
      throw new ForbiddenException(`You cannot create users with role ${role}`);
    }
  }

  private resolveSignupEstate(actor: RequestUser, dto: SignupDto) {
    if (this.isPlatformRole(dto.role)) {
      return null;
    }

    return resolveEstateScope(actor, dto.estateId);
  }

  private assertRoleSpecificFields(_dto: SignupDto) {
    return;
  }

  private async assertPlanRoleLimit(estateId: string | null, role: UserRole) {
    const limitKey = this.roleLimitKey(role);
    if (!estateId || !limitKey) {
      return;
    }

    const estate = await this.prisma.estate.findUnique({
      where: { id: estateId },
      select: { subscriptionPlan: { select: { limits: true } } }
    });
    const limit = getNumericPlanLimit(estate?.subscriptionPlan?.limits, limitKey);
    if (limit === null) {
      return;
    }

    const currentCount = await this.countEstateRole(estateId, role);
    if (currentCount >= limit) {
      throw new ConflictException(`${this.roleLabel(role)} limit for this estate plan has been reached`);
    }
  }

  private roleLimitKey(role: UserRole): PlanLimitKey | null {
    if (role === UserRole.RESIDENT) {
      return 'residents';
    }
    if (role === UserRole.GUARD) {
      return 'guards';
    }
    if (role === UserRole.SECURITY_SUPERVISOR) {
      return 'supervisors';
    }
    if (role === UserRole.ESTATE_ADMIN) {
      return 'estateAdmins';
    }
    return null;
  }

  private countEstateRole(estateId: string, role: UserRole) {
    if (role === UserRole.RESIDENT) {
      return this.prisma.resident.count({ where: { estateId } });
    }
    if (role === UserRole.GUARD) {
      return this.prisma.guard.count({ where: { estateId } });
    }
    return this.prisma.user.count({ where: { estateId, role } });
  }

  private roleLabel(role: UserRole) {
    return role.toLowerCase().replace(/_/g, ' ');
  }

  private requiredEstateIdForRole(role: UserRole): string {
    throw new BadRequestException(`estateId is required when signing up a ${role.toLowerCase()}`);
  }

  private requiredField(field: string): string {
    throw new BadRequestException(`${field} is required`);
  }

  private isPlatformRole(role: UserRole | string) {
    return role === UserRole.SUPER_ADMIN || role === UserRole.SALES_ADMIN;
  }
}
