import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { randomBytes, randomUUID } from 'node:crypto';

import { RedisService } from '../infra/redis.service';
import { AuthService } from './auth.service';

const QR_TTL_SECONDS = 120;

interface StoredQrSession {
  status: 'PENDING' | 'APPROVED' | 'EXPIRED';
  deviceName: string;
  fingerprint: string;
  deviceType?: string;
  ipAddress?: string;
  userAgent?: string;
  auth?: {
    accessToken: string;
    refreshToken: string;
    sessionId: string;
    pinVerified: boolean;
    expiresAt: string;
    requiresTotp?: boolean;
    totpChallengeToken?: string;
    user: Awaited<ReturnType<AuthService['toPublicUser']>>;
  };
  expiresAt: string;
}

interface CreateQrSessionInput {
  deviceName: string;
  fingerprint: string;
  deviceType?: string;
  ipAddress?: string;
  userAgent?: string;
}

@Injectable()
export class AdvancedAuthService {
  constructor(
    private readonly redis: RedisService,
    private readonly config: ConfigService,
    private readonly auth: AuthService
  ) {}

  createWebAuthnRegistrationChallenge(userId: string) {
    return this.challenge(`webauthn-register:${userId}`);
  }

  createWebAuthnLoginChallenge(userId: string) {
    return this.challenge(`webauthn-login:${userId}`);
  }

  async createQrSession(input: CreateQrSessionInput) {
    const sessionId = randomUUID();
    const expiresAt = new Date(Date.now() + QR_TTL_SECONDS * 1000);
    const apiBase = this.config.get<string>('PUBLIC_API_URL') ?? 'http://localhost:3001';
    const qrPayload = JSON.stringify({
      v: 1,
      type: 'idp_qr_login',
      sessionId,
      apiBase,
      expiresAt: expiresAt.toISOString()
    });

    const data: StoredQrSession = {
      status: 'PENDING',
      deviceName: input.deviceName,
      fingerprint: input.fingerprint,
      deviceType: input.deviceType,
      ipAddress: input.ipAddress,
      userAgent: input.userAgent,
      expiresAt: expiresAt.toISOString()
    };

    await this.redis.client.set(`qr:session:${sessionId}`, JSON.stringify(data), 'EX', QR_TTL_SECONDS);

    return {
      sessionId,
      status: 'PENDING',
      expiresAt: expiresAt.toISOString(),
      qrPayload
    };
  }

  async pollQrSession(sessionId: string) {
    const raw = await this.redis.client.get(`qr:session:${sessionId}`);
    if (!raw) {
      return { sessionId, status: 'EXPIRED', expiresAt: new Date().toISOString() };
    }

    const data = JSON.parse(raw) as StoredQrSession;
    if (data.status === 'PENDING' && new Date(data.expiresAt).getTime() <= Date.now()) {
      return { sessionId, status: 'EXPIRED', expiresAt: data.expiresAt };
    }

    return {
      sessionId,
      status: data.status,
      expiresAt: data.expiresAt,
      qrPayload: undefined,
      auth: data.auth
        ? {
            accessToken: data.auth.accessToken,
            refreshToken: data.auth.refreshToken,
            sessionId: data.auth.sessionId,
            pinVerified: data.auth.pinVerified,
            expiresAt: data.auth.expiresAt,
            requiresTotp: data.auth.requiresTotp,
            totpChallengeToken: data.auth.totpChallengeToken,
            user: data.auth.user
          }
        : undefined
    };
  }

  async approveQrSession(sessionId: string, userId: string) {
    const key = `qr:session:${sessionId}`;
    const raw = await this.redis.client.get(key);
    if (!raw) {
      throw new NotFoundException('QR-сессия не найдена или истекла');
    }

    const data = JSON.parse(raw) as StoredQrSession;
    if (data.status !== 'PENDING') {
      throw new BadRequestException('QR-сессия уже обработана');
    }
    if (new Date(data.expiresAt).getTime() <= Date.now()) {
      throw new BadRequestException('QR-сессия истекла');
    }

    const auth = await this.auth.createQrApprovedLogin(userId, {
      fingerprint: data.fingerprint,
      deviceName: data.deviceName,
      deviceType: data.deviceType,
      ipAddress: data.ipAddress,
      userAgent: data.userAgent
    });

    data.status = 'APPROVED';
    data.auth = auth;
    const ttl = await this.redis.client.ttl(key);
    await this.redis.client.set(key, JSON.stringify(data), 'EX', Math.max(ttl, 30));

    return {
      sessionId,
      status: 'APPROVED',
      expiresAt: data.expiresAt,
      auth: {
        accessToken: auth.accessToken,
        refreshToken: auth.refreshToken,
        sessionId: auth.sessionId,
        pinVerified: auth.pinVerified,
        expiresAt: auth.expiresAt,
        requiresTotp: auth.requiresTotp,
        totpChallengeToken: auth.totpChallengeToken,
        user: auth.user
      }
    };
  }

  private challenge(prefix: string) {
    return {
      challengeId: randomUUID(),
      challenge: `${prefix}:${randomBytes(32).toString('base64url')}`,
      expiresAt: new Date(Date.now() + 5 * 60_000).toISOString()
    };
  }
}
