import {
  Injectable,
  UnauthorizedException,
  ConflictException,
  BadRequestException,
  Logger,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import * as bcrypt from 'bcrypt';
import * as crypto from 'crypto';
import {
  User,
  DeviceRegistry,
  RefreshToken,
  Family,
  FamilyMember,
  AuditAction,
  EntityType,
} from '../../database/entities';
import { RegisterDto } from './dto/register.dto';
import { LoginDto } from './dto/login.dto';
import { RefreshTokenDto } from './dto/refresh-token.dto';
import { LogoutDto } from './dto/logout.dto';
import { AuthResponse, JwtPayload } from './interfaces/auth-response.interface';
import { FamilyRole } from '../../database/entities';
import { AuditService } from '../../common/services/audit.service';

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

  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
    @InjectRepository(DeviceRegistry)
    private deviceRepository: Repository<DeviceRegistry>,
    @InjectRepository(RefreshToken)
    private refreshTokenRepository: Repository<RefreshToken>,
    @InjectRepository(Family)
    private familyRepository: Repository<Family>,
    @InjectRepository(FamilyMember)
    private familyMemberRepository: Repository<FamilyMember>,
    private jwtService: JwtService,
    private configService: ConfigService,
    private auditService: AuditService,
  ) {}

  async register(registerDto: RegisterDto): Promise<AuthResponse> {
    // Check if user already exists
    const existingUser = await this.userRepository.findOne({
      where: { email: registerDto.email },
    });

    if (existingUser) {
      throw new ConflictException('User with this email already exists');
    }

    // COPPA Age Verification
    const birthDate = new Date(registerDto.dateOfBirth);
    const age = this.calculateAge(birthDate);

    // Block users under 13 (COPPA requirement)
    if (age < 13) {
      throw new BadRequestException(
        'Users must be at least 13 years old to create an account',
      );
    }

    // For users 13-17, require parental consent
    if (age >= 13 && age < 18) {
      if (!registerDto.coppaConsentGiven || !registerDto.parentalEmail) {
        throw new BadRequestException(
          'Users under 18 require parental consent. Please provide a parent/guardian email address.',
        );
      }
    }

    // Hash password
    const saltRounds = 10;
    const passwordHash = await bcrypt.hash(registerDto.password, saltRounds);

    // Create user
    const user = this.userRepository.create({
      email: registerDto.email,
      passwordHash,
      name: registerDto.name,
      phone: registerDto.phone,
      locale: registerDto.locale || 'en-US',
      timezone: registerDto.timezone || 'UTC',
      emailVerified: false,
      dateOfBirth: birthDate,
      coppaConsentGiven: registerDto.coppaConsentGiven || false,
      coppaConsentDate: registerDto.coppaConsentGiven ? new Date() : null,
      parentalEmail: registerDto.parentalEmail || null,
    });

    const savedUser = await this.userRepository.save(user);

    // Create default family for user
    const family = this.familyRepository.create({
      name: `${registerDto.name}'s Family`,
      createdBy: savedUser.id,
      subscriptionTier: 'free',
    });

    const savedFamily = await this.familyRepository.save(family);

    // Add user as parent in the family
    const familyMember = this.familyMemberRepository.create({
      userId: savedUser.id,
      familyId: savedFamily.id,
      role: FamilyRole.PARENT,
      permissions: {
        canAddChildren: true,
        canEditChildren: true,
        canLogActivities: true,
        canViewReports: true,
      },
    });

    await this.familyMemberRepository.save(familyMember);

    // Log COPPA consent if user is under 18
    if (age < 18 && registerDto.coppaConsentGiven) {
      await this.auditService.log({
        userId: savedUser.id,
        action: AuditAction.CONSENT_GRANTED,
        entityType: EntityType.USER,
        entityId: savedUser.id,
        changes: {
          after: {
            age,
            parentalEmail: registerDto.parentalEmail,
            consentDate: new Date(),
          },
        },
      });
    }

    // Register device
    const device = await this.registerDevice(
      savedUser.id,
      registerDto.deviceInfo.deviceId,
      registerDto.deviceInfo.platform,
    );

    // Generate tokens
    const tokens = await this.generateTokens(savedUser, device.id);

    return {
      success: true,
      data: {
        user: {
          id: savedUser.id,
          email: savedUser.email,
          name: savedUser.name,
          photoUrl: savedUser.photoUrl,
          locale: savedUser.locale,
          emailVerified: savedUser.emailVerified,
          preferences: savedUser.preferences,
          eulaAcceptedAt: savedUser.eulaAcceptedAt,
          eulaVersion: savedUser.eulaVersion,
        },
        family: {
          id: savedFamily.id,
          shareCode: savedFamily.shareCode,
          role: 'parent',
        },
        tokens,
        deviceRegistered: true,
      },
    };
  }

  async login(loginDto: LoginDto): Promise<AuthResponse> {
    // Find user
    const user = await this.userRepository.findOne({
      where: { email: loginDto.email },
      relations: ['familyMemberships', 'familyMemberships.family'],
    });

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

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

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

    // Check or register device
    let device = await this.deviceRepository.findOne({
      where: {
        userId: user.id,
        deviceFingerprint: loginDto.deviceInfo.deviceId,
      },
    });

    if (!device) {
      device = await this.registerDevice(
        user.id,
        loginDto.deviceInfo.deviceId,
        loginDto.deviceInfo.platform,
      );
    } else {
      // Update last seen
      device.lastSeen = new Date();
      await this.deviceRepository.save(device);
    }

    // Generate tokens
    const tokens = await this.generateTokens(user, device.id);

    // Get families with proper structure (matching /auth/me endpoint)
    const families =
      user.familyMemberships?.map((fm) => ({
        id: fm.familyId,
        familyId: fm.familyId,
        role: fm.role,
      })) || [];

    // Audit log: successful login
    await this.auditService.logLogin(user.id);

    return {
      success: true,
      data: {
        user: {
          id: user.id,
          email: user.email,
          name: user.name,
          photoUrl: user.photoUrl,
          locale: user.locale,
          emailVerified: user.emailVerified,
          preferences: user.preferences,
          families: families,
          eulaAcceptedAt: user.eulaAcceptedAt,
          eulaVersion: user.eulaVersion,
        },
        tokens,
        requiresMFA: false,
        deviceTrusted: device.trusted,
      },
    };
  }

  async refreshAccessToken(
    refreshTokenDto: RefreshTokenDto,
  ): Promise<AuthResponse> {
    try {
      // Verify refresh token signature and extract payload (contains deviceId)
      const payload = this.jwtService.verify(refreshTokenDto.refreshToken, {
        secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
      });

      const deviceIdFromPayload = payload.deviceId;
      const resolvedDeviceId = deviceIdFromPayload || refreshTokenDto.deviceId;

      if (!resolvedDeviceId) {
        this.logger.warn(
          `Refresh token request missing deviceId for user ${payload.sub}`,
        );
        throw new UnauthorizedException('Invalid refresh token');
      }

      if (
        deviceIdFromPayload &&
        refreshTokenDto.deviceId &&
        deviceIdFromPayload !== refreshTokenDto.deviceId
      ) {
        this.logger.debug(
          `Refresh token deviceId mismatch for user ${payload.sub}: token ${deviceIdFromPayload}, request ${refreshTokenDto.deviceId}`,
        );
      }

      // Hash the refresh token to compare with database
      const tokenHash = crypto
        .createHash('sha256')
        .update(refreshTokenDto.refreshToken)
        .digest('hex');

      // Find refresh token in database
      const refreshToken = await this.refreshTokenRepository.findOne({
        where: {
          tokenHash,
          userId: payload.sub,
          deviceId: resolvedDeviceId,
          revoked: false,
        },
        relations: ['user'],
      });

      if (!refreshToken) {
        throw new UnauthorizedException('Invalid refresh token');
      }

      // Check if token is expired
      if (new Date() > refreshToken.expiresAt) {
        throw new UnauthorizedException('Refresh token expired');
      }

      // Generate new tokens
      const tokens = await this.generateTokens(
        refreshToken.user,
        refreshToken.deviceId || resolvedDeviceId,
      );

      // Revoke old refresh token
      refreshToken.revoked = true;
      refreshToken.revokedAt = new Date();
      await this.refreshTokenRepository.save(refreshToken);

      return {
        success: true,
        data: {
          user: {
            id: refreshToken.user.id,
            email: refreshToken.user.email,
            name: refreshToken.user.name,
            photoUrl: refreshToken.user.photoUrl,
            locale: refreshToken.user.locale,
            emailVerified: refreshToken.user.emailVerified,
            preferences: refreshToken.user.preferences,
          },
          tokens,
        },
      };
    } catch (error) {
      throw new UnauthorizedException('Invalid or expired refresh token');
    }
  }

  async logout(
    userId: string,
    logoutDto: LogoutDto,
  ): Promise<{ success: boolean; message: string }> {
    if (logoutDto.allDevices) {
      // Revoke all refresh tokens for user
      await this.refreshTokenRepository.update(
        { userId, revoked: false },
        { revoked: true, revokedAt: new Date() },
      );
    } else {
      // Revoke refresh tokens for specific device
      await this.refreshTokenRepository.update(
        { userId, deviceId: logoutDto.deviceId, revoked: false },
        { revoked: true, revokedAt: new Date() },
      );
    }

    // Audit log: logout
    await this.auditService.logLogout(userId);

    return {
      success: true,
      message: 'Successfully logged out',
    };
  }

  async getUserById(userId: string): Promise<{ success: boolean; data: any }> {
    const user = await this.userRepository.findOne({
      where: { id: userId },
      relations: ['familyMemberships', 'familyMemberships.family'],
    });

    if (!user) {
      throw new UnauthorizedException('User not found');
    }

    const families =
      user.familyMemberships?.map((fm) => ({
        id: fm.familyId,
        familyId: fm.familyId,
        role: fm.role,
      })) || [];

    return {
      success: true,
      data: {
        id: user.id,
        email: user.email,
        name: user.name,
        photoUrl: user.photoUrl,
        role: 'user',
        locale: user.locale,
        timezone: user.timezone,
        emailVerified: user.emailVerified,
        preferences: user.preferences,
        families,
        eulaAcceptedAt: user.eulaAcceptedAt,
        eulaVersion: user.eulaVersion,
      },
    };
  }

  async updateProfile(
    userId: string,
    updateData: {
      name?: string;
      photoUrl?: string;
      timezone?: string;
      preferences?: {
        notifications?: boolean;
        emailUpdates?: boolean;
        darkMode?: boolean;
        measurementUnit?: 'metric' | 'imperial';
        timeFormat?: '12h' | '24h';
      };
    },
  ): Promise<{ success: boolean; data: any }> {
    this.logger.log(
      `updateProfile called for user ${userId} with data:`,
      updateData,
    );

    const user = await this.userRepository.findOne({
      where: { id: userId },
    });

    if (!user) {
      throw new UnauthorizedException('User not found');
    }

    this.logger.log(`Current user name: "${user.name}"`);
    this.logger.log(`New name: "${updateData.name}"`);

    // Update user fields if provided
    if (updateData.name !== undefined) {
      user.name = updateData.name;
      this.logger.log(`Updated user.name to: "${user.name}"`);
    }

    // Update photo URL if provided
    if (updateData.photoUrl !== undefined) {
      user.photoUrl = updateData.photoUrl;
      this.logger.log(`Updated user.photoUrl (length: ${updateData.photoUrl?.length || 0})`);
    }

    // Update timezone if provided
    if (updateData.timezone !== undefined) {
      user.timezone = updateData.timezone;
      this.logger.log(`Updated user.timezone to: "${user.timezone}"`);
    }

    // Update preferences if provided
    if (updateData.preferences !== undefined) {
      user.preferences = updateData.preferences;
      this.logger.log(`Updated user.preferences to:`, user.preferences);
    }

    const updatedUser = await this.userRepository.save(user);
    this.logger.log(
      `User saved. Updated name: "${updatedUser.name}", timezone: "${updatedUser.timezone}", preferences:`,
      updatedUser.preferences,
    );

    return {
      success: true,
      data: {
        id: updatedUser.id,
        email: updatedUser.email,
        name: updatedUser.name,
        photoUrl: updatedUser.photoUrl,
        role: 'user',
        locale: updatedUser.locale,
        timezone: updatedUser.timezone,
        emailVerified: updatedUser.emailVerified,
        preferences: updatedUser.preferences,
      },
    };
  }

  async validateUser(email: string, password: string): Promise<User | null> {
    const user = await this.userRepository.findOne({ where: { email } });

    if (user && (await bcrypt.compare(password, user.passwordHash))) {
      return user;
    }

    return null;
  }

  private async registerDevice(
    userId: string,
    deviceFingerprint: string,
    platform: string,
  ): Promise<DeviceRegistry> {
    const device = this.deviceRepository.create({
      userId,
      deviceFingerprint,
      platform,
      trusted: false, // New devices are not trusted by default
    });

    return await this.deviceRepository.save(device);
  }

  private async generateTokens(
    user: User,
    deviceId: string,
  ): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
    const payload: JwtPayload = {
      sub: user.id,
      email: user.email,
      deviceId,
    };

    // Generate access token
    const accessToken = this.jwtService.sign(payload, {
      secret: this.configService.get<string>('JWT_SECRET'),
      expiresIn: this.configService.get<string>('JWT_EXPIRATION', '1h'),
    });

    // Generate refresh token
    const refreshToken = this.jwtService.sign(payload, {
      secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
      expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRATION', '7d'),
    });

    // Store refresh token hash in database
    const tokenHash = crypto
      .createHash('sha256')
      .update(refreshToken)
      .digest('hex');

    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + 7); // 7 days

    const refreshTokenEntity = this.refreshTokenRepository.create({
      userId: user.id,
      deviceId,
      tokenHash,
      expiresAt,
    });

    await this.refreshTokenRepository.save(refreshTokenEntity);

    return {
      accessToken,
      refreshToken,
      expiresIn: 3600, // 1 hour in seconds
    };
  }

  /**
   * Login with external authentication (e.g., biometric, OAuth)
   * Skips password validation but performs device registration and token generation
   */
  async loginWithExternalAuth(
    user: User,
    deviceInfo: { deviceId: string; platform: string },
  ): Promise<AuthResponse> {
    // Register or update device
    const device = await this.registerDevice(
      user.id,
      deviceInfo.deviceId,
      deviceInfo.platform,
    );

    // Generate JWT tokens
    const tokens = await this.generateTokens(user, device.id);

    // Audit log for biometric login
    await this.auditService.log({
      userId: user.id,
      action: AuditAction.LOGIN_BIOMETRIC,
      entityType: EntityType.USER,
      entityId: user.id,
      changes: {
        after: {
          deviceId: device.deviceFingerprint,
          platform: device.platform,
        },
      },
    });

    return {
      success: true,
      data: {
        tokens: {
          accessToken: tokens.accessToken,
          refreshToken: tokens.refreshToken,
          expiresIn: tokens.expiresIn,
        },
        user: {
          id: user.id,
          email: user.email,
          name: user.name,
          photoUrl: user.photoUrl,
          locale: user.locale,
          emailVerified: user.emailVerified,
          preferences: user.preferences,
          eulaAcceptedAt: user.eulaAcceptedAt,
          eulaVersion: user.eulaVersion,
        },
        deviceRegistered: true,
        deviceTrusted: device.trusted,
      },
    };
  }

  /**
   * Calculate age from date of birth
   */
  private calculateAge(birthDate: Date): number {
    const today = new Date();
    let age = today.getFullYear() - birthDate.getFullYear();
    const monthDiff = today.getMonth() - birthDate.getMonth();

    if (
      monthDiff < 0 ||
      (monthDiff === 0 && today.getDate() < birthDate.getDate())
    ) {
      age--;
    }

    return age;
  }

  /**
   * Accept EULA for user
   */
  async acceptEULA(userId: string, version: string = '2025-10-04'): Promise<any> {
    const user = await this.userRepository.findOne({
      where: { id: userId },
    });

    if (!user) {
      throw new UnauthorizedException('User not found');
    }

    // Update EULA acceptance
    user.eulaAcceptedAt = new Date();
    user.eulaVersion = version;

    await this.userRepository.save(user);

    // Audit log for EULA acceptance
    await this.auditService.log({
      userId: user.id,
      action: AuditAction.CONSENT_GRANTED,
      entityType: EntityType.USER,
      entityId: user.id,
      changes: {
        after: { eulaAcceptedAt: user.eulaAcceptedAt, eulaVersion: version },
      },
    });

    this.logger.log(`User ${userId} accepted EULA version ${version}`);

    return {
      success: true,
      message: 'EULA accepted successfully',
      eulaAcceptedAt: user.eulaAcceptedAt,
      eulaVersion: user.eulaVersion,
    };
  }
}
