import {
  BadRequestException,
  ConflictException,
  Inject,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { createHash, randomBytes } from 'crypto';
import { UserStatus } from '../common/constants/enums';
import { UsersService } from '../users/users.service';
import { AuditLogsService } from '../audit-logs/audit-logs.service';
import { EMAIL_PROVIDER } from '../email/email-provider.interface';
import type { EmailProvider } from '../email/email-provider.interface';
import type { JwtUserPayload } from './jwt.types';
import {
  parseDurationMs,
  resolveJwtAccessSecret,
} from '../config/runtime-config';
import {
  RefreshToken,
  RefreshTokenDocument,
} from './schemas/refresh-token.schema';
import {
  PasswordResetToken,
  PasswordResetTokenDocument,
} from './schemas/password-reset-token.schema';
import { docId } from '../common/utils/mongo.util';
import { UserDocument } from '../users/schemas/user.schema';

@Injectable()
export class AuthService {
  constructor(
    private readonly users: UsersService,
    private readonly jwt: JwtService,
    private readonly config: ConfigService,
    private readonly audit: AuditLogsService,
    @InjectModel(RefreshToken.name)
    private readonly refreshModel: Model<RefreshTokenDocument>,
    @InjectModel(PasswordResetToken.name)
    private readonly resetModel: Model<PasswordResetTokenDocument>,
    @Inject(EMAIL_PROVIDER) private readonly email: EmailProvider,
  ) {}

  private refreshTtlMs(): number {
    return parseDurationMs(
      this.config.get<string>('JWT_REFRESH_EXPIRES', '7d') ?? '7d',
      7 * 24 * 60 * 60 * 1000,
    );
  }

  private signAccess(user: UserDocument) {
    const payload: JwtUserPayload = {
      sub: docId(user),
      uid: user.uid,
      email: user.email,
      role: user.role,
      deviceId: user.deviceId,
    };
    return this.jwt.sign(payload, {
      secret: resolveJwtAccessSecret(this.config),
      expiresIn: this.config.get('JWT_ACCESS_EXPIRES', '15m'),
    });
  }

  private async issueRefresh(userId: string, deviceId?: string): Promise<string> {
    const raw = randomBytes(48).toString('base64url');
    const tokenHash = createHash('sha256').update(raw).digest('hex');
    await this.refreshModel.create({
      userId,
      tokenHash,
      deviceId,
      expiresAt: new Date(Date.now() + this.refreshTtlMs()),
    });
    return raw;
  }

  async login(dto: {
    email: string;
    password: string;
    deviceId?: string;
    isRooted?: boolean;
  }) {
    const user = await this.users.findByEmail(dto.email);
    if (!user || user.status === UserStatus.INACTIVE) {
      throw new UnauthorizedException('Invalid credentials');
    }
    const ok = await this.users.verifyPassword(user, dto.password);
    if (!ok) throw new UnauthorizedException('Invalid credentials');

    await this.users.ensureCanonicalRole(user);

    if (dto.deviceId) {
      if (user.deviceId && user.deviceId !== dto.deviceId) {
        throw new ConflictException('Account is bound to another device');
      }
      if (!user.deviceId) {
        await this.users.bindDevice(user.uid, dto.deviceId, dto.isRooted);
        user.deviceId = dto.deviceId;
      }
    }

    if (dto.isRooted) {
      await this.audit.append({
        actorUserId: docId(user),
        actorEmail: user.email,
        action: 'rooted_device_login',
        targetType: 'user',
        targetId: user.uid,
        metadata: { deviceId: dto.deviceId },
      });
    }

    const accessToken = this.signAccess(user);
    const refreshToken = await this.issueRefresh(docId(user), dto.deviceId);
    return {
      accessToken,
      refreshToken,
      user: await this.users.toClientUserWithManager(user),
    };
  }

  async refresh(refreshToken: string, deviceId?: string) {
    const tokenHash = createHash('sha256').update(refreshToken).digest('hex');
    const row = await this.refreshModel.findOne({ tokenHash }).exec();
    if (!row || row.expiresAt.getTime() < Date.now()) {
      throw new UnauthorizedException('Invalid refresh token');
    }
    const user = await this.users.findById(row.userId);
    if (!user || user.status === UserStatus.INACTIVE) {
      throw new UnauthorizedException('Invalid refresh token');
    }
    await this.users.assertDeviceMatch(user, deviceId);
    await this.refreshModel.deleteOne({ _id: row._id });
    const accessToken = this.signAccess(user);
    const nextRefresh = await this.issueRefresh(docId(user), deviceId);
    return {
      accessToken,
      refreshToken: nextRefresh,
      user: await this.users.toClientUserWithManager(user),
    };
  }

  async logout(params: { refreshToken?: string; userId?: string }) {
    if (params.refreshToken?.trim()) {
      const tokenHash = createHash('sha256')
        .update(params.refreshToken.trim())
        .digest('hex');
      await this.refreshModel.deleteOne({ tokenHash });
    } else if (params.userId) {
      await this.refreshModel.deleteMany({ userId: params.userId });
    }
    return { ok: true };
  }

  async forgotPassword(email: string) {
    const user = await this.users.findByEmail(email);
    if (!user) return { ok: true };
    const raw = randomBytes(32).toString('hex');
    const tokenHash = createHash('sha256').update(raw).digest('hex');
    await this.resetModel.deleteMany({ email: user.email });
    await this.resetModel.create({
      email: user.email,
      tokenHash,
      expiresAt: new Date(Date.now() + 60 * 60 * 1000),
    });
    await this.email.sendPasswordReset(user.email, raw);
    return { ok: true };
  }

  async resetPassword(token: string, newPassword: string) {
    const tokenHash = createHash('sha256').update(token).digest('hex');
    const row = await this.resetModel
      .findOne({ tokenHash, consumed: false })
      .exec();
    if (!row || row.expiresAt.getTime() < Date.now()) {
      throw new BadRequestException('Invalid or expired reset token');
    }
    const user = await this.users.findByEmail(row.email);
    if (!user) throw new BadRequestException('Invalid reset token');
    await this.users.updatePassword(user.uid, newPassword);
    row.consumed = true;
    await row.save();
    await this.audit.append({
      actorUserId: docId(user),
      actorEmail: user.email,
      action: 'password_reset_completed',
      targetType: 'user',
      targetId: user.uid,
    });
    return { ok: true };
  }

  async cleanupExpiredTokens() {
    const now = new Date();
    await this.refreshModel.deleteMany({ expiresAt: { $lt: now } });
    await this.resetModel.deleteMany({ expiresAt: { $lt: now } });
  }
}
