import { BadRequestException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';

import * as bcrypt from 'bcryptjs';

import { SessionStatus, UserStatus } from '../generated/prisma/client';

import { PrismaService } from '../infra/prisma.service';
import { OtpService } from './otp.service';
import { TotpService } from './totp.service';
import { SettingsService } from './settings.service';
import { FamilyService } from './family.service';



interface UpdateProfileCommand {
  userId: string;
  firstName?: string;
  lastName?: string;
  bio?: string;
  age?: number;
  gender?: string;
  birthDate?: string;
}



interface UpdateContactsCommand {

  userId: string;

  email?: string;

  phone?: string;

  backupEmail?: string;

  backupPhone?: string;

}



export function buildDeletedFieldValue(userId: string, value: string): string {

  const prefix = `is_deleted_${userId}_`;

  if (value.startsWith(prefix)) return value;

  return `${prefix}${value}`;

}



function prefixNullable(userId: string, value: string | null | undefined): string | null {

  if (!value) return null;

  return buildDeletedFieldValue(userId, value);

}



interface PasswordVerificationCommand {
  currentPassword?: string;
  otpCode?: string;
  otpChannel?: string;
  totpCode?: string;
}

@Injectable()

export class ProfileService {

  constructor(
    private readonly prisma: PrismaService,
    private readonly otp: OtpService,
    private readonly totp: TotpService,
    private readonly settings: SettingsService,
    private readonly family: FamilyService
  ) {}



  async getProfile(userId: string) {

    const user = await this.prisma.user.findUnique({

      where: { id: userId },

      include: { profile: true }

    });

    if (!user) {

      throw new NotFoundException('Пользователь не найден');

    }

    return this.toResponse(user);

  }

  async setE2EPublicKey(userId: string, publicKey: string) {
    const trimmed = publicKey.trim();
    if (!trimmed || trimmed.length > 4096) {
      throw new BadRequestException('Некорректный публичный ключ E2E');
    }

    const user = await this.prisma.user.update({
      where: { id: userId },
      data: { e2ePublicKey: trimmed },
      include: { profile: true }
    });

    return { userId: user.id, e2ePublicKey: user.e2ePublicKey ?? undefined };
  }

  async getE2EPublicKey(userId: string) {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { id: true, e2ePublicKey: true }
    });
    if (!user) {
      throw new NotFoundException('Пользователь не найден');
    }
    return { userId: user.id, e2ePublicKey: user.e2ePublicKey ?? undefined };
  }



  async updateAvatar(userId: string, avatarUrl?: string, avatarStorageKey?: string) {

    if (!avatarStorageKey && !avatarUrl) {

      throw new BadRequestException('Укажите ключ хранилища или ссылку на аватар');

    }



    if (avatarStorageKey) {

      if (!avatarStorageKey.startsWith(`avatars/${userId}/`)) {

        throw new BadRequestException('Некорректный ключ аватара');

      }

      const user = await this.prisma.user.update({

        where: { id: userId },

        data: { avatarStorageKey, avatarUrl: null },

        include: { profile: true }

      });

      return this.toResponse(user);

    }



    if (!avatarUrl || !/^https?:\/\/.+/i.test(avatarUrl)) {

      throw new BadRequestException('Укажите корректную ссылку на аватар или ключ хранилища');

    }

    const user = await this.prisma.user.update({

      where: { id: userId },

      data: { avatarUrl, avatarStorageKey: null },

      include: { profile: true }

    });

    return this.toResponse(user);

  }



  async updateProfile(command: UpdateProfileCommand) {
    const displayName = [command.firstName, command.lastName].filter(Boolean).join(' ').trim();
    const birthDate = command.birthDate ? this.parseBirthDate(command.birthDate) : undefined;
    const user = await this.prisma.user.update({
      where: { id: command.userId },
      data: {
        displayName: displayName || undefined,
        gender: command.gender,
        birthDate,
        profile: {
          upsert: {
            create: {
              firstName: command.firstName,
              lastName: command.lastName,
              bio: command.bio,
              age: command.age,
              gender: command.gender
            },
            update: {
              firstName: command.firstName,
              lastName: command.lastName,
              bio: command.bio,
              age: command.age,
              gender: command.gender
            }
          }
        }
      },
      include: { profile: true }
    });
    return this.toResponse(user);
  }



  async updateContacts(command: UpdateContactsCommand) {

    const user = await this.prisma.user.update({

      where: { id: command.userId },

      data: {

        email: command.email,

        phone: command.phone,

        backupEmail: command.backupEmail,

        backupPhone: command.backupPhone

      },

      include: { profile: true }

    });

    return this.toResponse(user);

  }



  async setPassword(userId: string, password: string) {

    if (!password || password.length < 8) {

      throw new BadRequestException('Пароль должен содержать минимум 8 символов');

    }

    const existing = await this.prisma.user.findUnique({ where: { id: userId } });

    if (!existing) {

      throw new NotFoundException('Пользователь не найден');

    }

    if (existing.passwordHash) {

      throw new BadRequestException('Пароль уже установлен. Для смены подтвердите личность.');

    }

    const user = await this.prisma.user.update({

      where: { id: userId },

      data: { passwordHash: await bcrypt.hash(password, 12) }

    });

    return { hasPassword: Boolean(user.passwordHash) };

  }

  async sendPasswordVerificationOtp(userId: string, channel: 'sms' | 'email') {

    const user = await this.requireUserWithPassword(userId);
    const target = channel === 'sms' ? user.phone ?? user.backupPhone : user.email ?? user.backupEmail;

    if (!target) {

      throw new BadRequestException(
        channel === 'sms' ? 'Не указан телефон для отправки SMS-кода' : 'Не указана почта для отправки кода'
      );

    }

    await this.otp.send({
      target,
      channel,
      purpose: 'password-change',
      userId
    });

    return { sent: true, maskedTarget: this.maskVerificationTarget(target, channel) };

  }

  async changePassword(userId: string, newPassword: string, verification: PasswordVerificationCommand) {

    if (!newPassword || newPassword.length < 8) {

      throw new BadRequestException('Новый пароль должен содержать минимум 8 символов');

    }

    const user = await this.requireUserWithPassword(userId);
    await this.assertPasswordVerification(userId, user, verification);

    const updated = await this.prisma.user.update({

      where: { id: userId },

      data: { passwordHash: await bcrypt.hash(newPassword, 12) }

    });

    return { hasPassword: Boolean(updated.passwordHash) };

  }

  async removePassword(userId: string, verification: PasswordVerificationCommand) {

    const user = await this.requireUserWithPassword(userId);
    await this.assertPasswordVerification(userId, user, verification);

    await this.prisma.user.update({

      where: { id: userId },

      data: { passwordHash: null }

    });

    return { hasPassword: false };

  }

  private async requireUserWithPassword(userId: string) {

    const user = await this.prisma.user.findUnique({ where: { id: userId } });

    if (!user) {

      throw new NotFoundException('Пользователь не найден');

    }

    if (!user.passwordHash) {

      throw new BadRequestException('Пароль не установлен');

    }

    return user;

  }

  private async assertPasswordVerification(
    userId: string,
    user: { passwordHash: string | null; email: string | null; phone: string | null; backupEmail: string | null; backupPhone: string | null },
    verification: PasswordVerificationCommand
  ) {

    if (verification.currentPassword?.trim()) {

      const matches = await bcrypt.compare(verification.currentPassword.trim(), user.passwordHash!);

      if (!matches) {

        throw new UnauthorizedException('Неверный текущий пароль');

      }

      return;

    }

    if (verification.otpCode?.trim() && verification.otpChannel) {

      const channel = verification.otpChannel === 'sms' ? 'sms' : 'email';

      const target = channel === 'sms' ? user.phone ?? user.backupPhone : user.email ?? user.backupEmail;

      if (!target) {

        throw new BadRequestException('Контакт для проверки OTP не найден');

      }

      await this.otp.verify({ target, code: verification.otpCode.trim(), purpose: 'password-change' });

      return;

    }

    if (verification.totpCode?.trim()) {

      const isValid = await this.totp.verifyEnabledCode(userId, verification.totpCode.trim());

      if (!isValid) {

        throw new UnauthorizedException('Неверный код аутентификатора');

      }

      return;

    }

    throw new BadRequestException(
      'Подтвердите личность: укажите текущий пароль, код OTP или код из приложения-аутентификатора'
    );

  }

  private maskVerificationTarget(target: string, channel: 'sms' | 'email') {

    if (channel === 'email' && target.includes('@')) {

      const [name, domain] = target.split('@');

      return `${name.slice(0, 1)}***@${domain}`;

    }

    const digits = target.replace(/\D/g, '');

    return `***${digits.slice(-4)}`;

  }



  async requestAccountDeletion(userId: string) {
    const user = await this.prisma.user.findUnique({ where: { id: userId } });
    if (!user) {
      throw new NotFoundException('Пользователь не найден');
    }
    if (user.status === UserStatus.DELETED || user.deletedAt) {
      throw new BadRequestException('Профиль уже удалён');
    }
    if (user.deletionRequestedAt) {
      throw new BadRequestException('Удаление аккаунта уже запланировано');
    }

    const graceDays = await this.settings.getNumber('ACCOUNT_DELETE_GRACE_DAYS', 30);
    const deletionRequestedAt = new Date();
    const effectiveAt = new Date(deletionRequestedAt.getTime() + graceDays * 24 * 60 * 60 * 1000);

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

    return {
      userId,
      deletionRequestedAt: deletionRequestedAt.toISOString(),
      effectiveAt: effectiveAt.toISOString(),
      graceDays
    };
  }

  async cancelAccountDeletion(userId: string) {
    const user = await this.prisma.user.findUnique({ where: { id: userId } });
    if (!user) {
      throw new NotFoundException('Пользователь не найден');
    }
    if (!user.deletionRequestedAt) {
      throw new BadRequestException('Запрос на удаление аккаунта не найден');
    }

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

    return { userId, cancelled: true };
  }

  async getAccountDeletionStatus(userId: string) {
    const user = await this.prisma.user.findUnique({ where: { id: userId } });
    if (!user) {
      throw new NotFoundException('Пользователь не найден');
    }

    const graceDays = await this.settings.getNumber('ACCOUNT_DELETE_GRACE_DAYS', 30);
    if (!user.deletionRequestedAt) {
      return { pending: false, graceDays };
    }

    const effectiveAt = new Date(user.deletionRequestedAt.getTime() + graceDays * 24 * 60 * 60 * 1000);
    return {
      pending: true,
      deletionRequestedAt: user.deletionRequestedAt.toISOString(),
      effectiveAt: effectiveAt.toISOString(),
      graceDays
    };
  }

  async finalizeDueAccountDeletions() {
    const graceDays = await this.settings.getNumber('ACCOUNT_DELETE_GRACE_DAYS', 30);
    const threshold = new Date(Date.now() - graceDays * 24 * 60 * 60 * 1000);
    const due = await this.prisma.user.findMany({
      where: {
        status: UserStatus.ACTIVE,
        deletionRequestedAt: { not: null, lte: threshold }
      },
      select: { id: true }
    });

    for (const user of due) {
      await this.finalizeAccountDeletion(user.id);
    }

    return { count: due.length };
  }

  async finalizeAccountDeletion(userId: string) {
    await this.family.destroyOwnedGroupsForUser(userId);
    await this.family.leaveAllMembershipsForUser(userId);
    await this.softDeleteProfile(userId);
  }

  async softDeleteProfile(userId: string) {

    const user = await this.prisma.user.findUnique({

      where: { id: userId },

      include: { profile: true, linkedAccounts: true }

    });



    if (!user) {

      throw new NotFoundException('Пользователь не найден');

    }



    if (user.status === UserStatus.DELETED || user.deletedAt) {

      throw new BadRequestException('Профиль уже удалён');

    }



    await this.prisma.$transaction(async (tx) => {

      await tx.session.updateMany({

        where: { userId, status: { in: [SessionStatus.ACTIVE, SessionStatus.LOCKED] } },

        data: { status: SessionStatus.REVOKED }

      });



      await tx.userRole.deleteMany({ where: { userId } });



      await tx.user.update({

        where: { id: userId },

        data: {

          email: prefixNullable(userId, user.email),

          phone: prefixNullable(userId, user.phone),

          backupEmail: prefixNullable(userId, user.backupEmail),

          backupPhone: prefixNullable(userId, user.backupPhone),

          username: prefixNullable(userId, user.username),

          displayName: buildDeletedFieldValue(userId, user.displayName),

          avatarUrl: null,

          avatarStorageKey: null,

          passwordHash: null,

          isSuperAdmin: false,

          status: UserStatus.DELETED,

          deletedAt: new Date(),

          deletionRequestedAt: null

        }

      });



      if (user.profile) {

        await tx.userProfile.update({

          where: { userId },

          data: {

            firstName: prefixNullable(userId, user.profile.firstName),

            lastName: prefixNullable(userId, user.profile.lastName),

            bio: prefixNullable(userId, user.profile.bio)

          }

        });

      }



      for (const account of user.linkedAccounts) {

        await tx.linkedAccount.update({

          where: { id: account.id },

          data: {

            providerId: buildDeletedFieldValue(userId, account.providerId),

            email: prefixNullable(userId, account.email),

            displayName: account.displayName ? prefixNullable(userId, account.displayName) : null

          }

        });

      }

    });



    return { success: true };

  }



  private toResponse(user: Awaited<ReturnType<PrismaService['user']['findUnique']>> & { profile?: { id?: string; firstName?: string | null; lastName?: string | null; bio?: string | null; age?: number | null; gender?: string | null } | null }) {

    if (!user) {

      throw new NotFoundException('Пользователь не найден');

    }

    return {

      id: user.profile?.id ?? user.id,

      userId: user.id,

      displayName: user.displayName,

      avatarUrl: user.avatarUrl,

      hasAvatar: Boolean(user.avatarStorageKey || user.avatarUrl),

      firstName: user.profile?.firstName,

      lastName: user.profile?.lastName,

      bio: user.profile?.bio,

      age: user.profile?.age,

      gender: user.profile?.gender ?? user.gender,

      email: user.email,

      phone: user.phone,

      backupEmail: user.backupEmail,

      backupPhone: user.backupPhone,

      birthDate: user.birthDate?.toISOString().slice(0, 10) ?? null,

      hasPassword: Boolean(user.passwordHash),

      deletionRequestedAt: (user as { deletionRequestedAt?: Date | null }).deletionRequestedAt?.toISOString(),

      e2ePublicKey: (user as { e2ePublicKey?: string | null }).e2ePublicKey ?? undefined

    };

  }

  private parseBirthDate(value: string): Date {
    if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
      throw new BadRequestException('Укажите корректную дату рождения');
    }
    const parsed = new Date(`${value}T00:00:00.000Z`);
    if (Number.isNaN(parsed.getTime())) {
      throw new BadRequestException('Укажите корректную дату рождения');
    }
    const today = new Date();
    today.setUTCHours(0, 0, 0, 0);
    if (parsed.getTime() > today.getTime()) {
      throw new BadRequestException('Дата рождения не может быть в будущем');
    }
    return parsed;
  }

}


