import {
  Injectable,
  UnauthorizedException,
  ConflictException,
  Logger,
  Inject,
  BadRequestException,
  NotFoundException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import * as argon2 from 'argon2';
import * as crypto from 'crypto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, IsNull } from 'typeorm';
import { UserRepository, USER_REPOSITORY } from '@domain/ports/out/user.repository';
import { User, UserRole } from '@domain/entities/user.entity';
import {
  OrganizationRepository,
  ORGANIZATION_REPOSITORY,
} from '@domain/ports/out/organization.repository';
import { Organization } from '@domain/entities/organization.entity';
import { EmailPort, EMAIL_PORT } from '@domain/ports/out/email.port';
import { v4 as uuidv4 } from 'uuid';
import { RegisterOrganizationDto } from '../dto/auth-login.dto';
import { SubscriptionService } from '../services/subscription.service';
import { PasswordResetTokenOrmEntity } from '../../infrastructure/persistence/typeorm/entities/password-reset-token.orm-entity';

export interface JwtPayload {
  sub: string; // user ID
  email: string;
  role: string;
  organizationId: string;
  plan?: string; // subscription plan (BRONZE, SILVER, GOLD, PLATINIUM)
  planFeatures?: string[]; // plan feature flags
  type: 'access' | 'refresh';
}

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

  constructor(
    @Inject(USER_REPOSITORY)
    private readonly userRepository: UserRepository,
    @Inject(ORGANIZATION_REPOSITORY)
    private readonly organizationRepository: OrganizationRepository,
    @Inject(EMAIL_PORT)
    private readonly emailService: EmailPort,
    @InjectRepository(PasswordResetTokenOrmEntity)
    private readonly passwordResetTokenRepository: Repository<PasswordResetTokenOrmEntity>,
    private readonly jwtService: JwtService,
    private readonly configService: ConfigService,
    private readonly subscriptionService: SubscriptionService
  ) {}

  /**
   * Register a new user
   */
  async register(
    email: string,
    password: string,
    firstName: string,
    lastName: string,
    organizationId?: string,
    organizationData?: RegisterOrganizationDto,
    invitationRole?: string
  ): Promise<{ accessToken: string; refreshToken: string; user: any }> {
    this.logger.log(`Registering new user: ${email}`);

    const existingUser = await this.userRepository.findByEmail(email);

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

    const passwordHash = await argon2.hash(password, {
      type: argon2.argon2id,
      memoryCost: 65536, // 64 MB
      timeCost: 3,
      parallelism: 4,
    });

    // Determine organization ID:
    // 1. If organizationId is provided (invited user), use it
    // 2. If organizationData is provided (new user), create a new organization
    // 3. Otherwise, use default organization
    const finalOrganizationId = await this.resolveOrganizationId(organizationId, organizationData);

    // Determine role:
    // - If invitation role is provided (invited user), use it
    // - If organizationData is provided (new organization creator), make them MANAGER
    // - Otherwise, default to USER
    let userRole: UserRole;
    if (invitationRole) {
      userRole = invitationRole as UserRole;
    } else if (organizationData) {
      // User creating a new organization becomes MANAGER
      userRole = UserRole.MANAGER;
    } else {
      // Default to USER for other cases
      userRole = UserRole.USER;
    }

    const user = User.create({
      id: uuidv4(),
      organizationId: finalOrganizationId,
      email,
      passwordHash,
      firstName,
      lastName,
      role: userRole,
    });

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

    // Allocate a license for the new user
    try {
      await this.subscriptionService.allocateLicense(savedUser.id, finalOrganizationId);
      this.logger.log(`License allocated for user: ${email}`);
    } catch (error) {
      this.logger.error(`Failed to allocate license for user ${email}:`, error);
      // Note: We don't throw here because the user is already created.
      // The license check should happen before invitation.
    }

    const tokens = await this.generateTokens(savedUser);

    this.logger.log(`User registered successfully: ${email}`);

    return {
      ...tokens,
      user: {
        id: savedUser.id,
        email: savedUser.email,
        firstName: savedUser.firstName,
        lastName: savedUser.lastName,
        role: savedUser.role,
        organizationId: savedUser.organizationId,
      },
    };
  }

  /**
   * Login user with email and password
   */
  async login(
    email: string,
    password: string
  ): Promise<{ accessToken: string; refreshToken: string; user: any }> {
    this.logger.log(`Login attempt for: ${email}`);

    const user = await this.userRepository.findByEmail(email);

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

    if (!user.isActive) {
      throw new UnauthorizedException('User account is inactive');
    }

    const isPasswordValid = await argon2.verify(user.passwordHash, password);

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

    const tokens = await this.generateTokens(user);

    this.logger.log(`User logged in successfully: ${email}`);

    return {
      ...tokens,
      user: {
        id: user.id,
        email: user.email,
        firstName: user.firstName,
        lastName: user.lastName,
        role: user.role,
        organizationId: user.organizationId,
      },
    };
  }

  /**
   * Refresh access token using refresh token
   */
  async refreshAccessToken(
    refreshToken: string
  ): Promise<{ accessToken: string; refreshToken: string }> {
    try {
      const payload = await this.jwtService.verifyAsync<JwtPayload>(refreshToken, {
        secret: this.configService.get('JWT_SECRET'),
      });

      if (payload.type !== 'refresh') {
        throw new UnauthorizedException('Invalid token type');
      }

      const user = await this.userRepository.findById(payload.sub);

      if (!user || !user.isActive) {
        throw new UnauthorizedException('User not found or inactive');
      }

      const tokens = await this.generateTokens(user);

      this.logger.log(`Access token refreshed for user: ${user.email}`);

      return tokens;
    } catch (error: any) {
      this.logger.error(`Token refresh failed: ${error?.message || 'Unknown error'}`);
      throw new UnauthorizedException('Invalid or expired refresh token');
    }
  }

  /**
   * Initiate password reset — generates token and sends email
   */
  async forgotPassword(email: string): Promise<void> {
    this.logger.log(`Password reset requested for: ${email}`);

    const user = await this.userRepository.findByEmail(email);

    // Silently succeed if user not found (security: don't reveal user existence)
    if (!user || !user.isActive) {
      return;
    }

    // Invalidate any existing unused tokens for this user
    await this.passwordResetTokenRepository.update(
      { userId: user.id, usedAt: IsNull() },
      { usedAt: new Date() }
    );

    // Generate a secure random token
    const token = crypto.randomBytes(32).toString('hex');
    const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour

    await this.passwordResetTokenRepository.save({
      userId: user.id,
      token,
      expiresAt,
      usedAt: null,
    });

    await this.emailService.sendPasswordResetEmail(email, token);

    this.logger.log(`Password reset email sent to: ${email}`);
  }

  /**
   * Reset password using token from email
   */
  async resetPassword(token: string, newPassword: string): Promise<void> {
    const resetToken = await this.passwordResetTokenRepository.findOne({ where: { token } });

    if (!resetToken) {
      throw new BadRequestException('Token de réinitialisation invalide ou expiré');
    }

    if (resetToken.usedAt) {
      throw new BadRequestException('Ce lien de réinitialisation a déjà été utilisé');
    }

    if (resetToken.expiresAt < new Date()) {
      throw new BadRequestException(
        'Le lien de réinitialisation a expiré. Veuillez en demander un nouveau.'
      );
    }

    const user = await this.userRepository.findById(resetToken.userId);

    if (!user || !user.isActive) {
      throw new NotFoundException('Utilisateur introuvable');
    }

    const passwordHash = await argon2.hash(newPassword, {
      type: argon2.argon2id,
      memoryCost: 65536,
      timeCost: 3,
      parallelism: 4,
    });

    // Update password (mutates in place)
    user.updatePassword(passwordHash);
    await this.userRepository.save(user);

    // Mark token as used
    await this.passwordResetTokenRepository.update({ id: resetToken.id }, { usedAt: new Date() });

    this.logger.log(`Password reset successfully for user: ${user.email}`);
  }

  /**
   * Validate user from JWT payload
   */
  async validateUser(payload: JwtPayload): Promise<User | null> {
    const user = await this.userRepository.findById(payload.sub);

    if (!user || !user.isActive) {
      return null;
    }

    return user;
  }

  /**
   * Generate access and refresh tokens
   */
  private async generateTokens(user: User): Promise<{ accessToken: string; refreshToken: string }> {
    // ADMIN users always get PLATINIUM plan with no expiration
    let plan = 'BRONZE';
    let planFeatures: string[] = [];

    if (user.role === UserRole.ADMIN) {
      plan = 'PLATINIUM';
      planFeatures = [
        'dashboard',
        'wiki',
        'user_management',
        'csv_export',
        'api_access',
        'custom_interface',
        'dedicated_kam',
      ];
    } else {
      try {
        const subscription = await this.subscriptionService.getOrCreateSubscription(
          user.organizationId
        );
        plan = subscription.plan.value;
        planFeatures = [...subscription.plan.planFeatures];
      } catch (error) {
        this.logger.warn(`Failed to fetch subscription for JWT: ${error}`);
      }
    }

    const accessPayload: JwtPayload = {
      sub: user.id,
      email: user.email,
      role: user.role,
      organizationId: user.organizationId,
      plan,
      planFeatures,
      type: 'access',
    };

    const refreshPayload: JwtPayload = {
      sub: user.id,
      email: user.email,
      role: user.role,
      organizationId: user.organizationId,
      plan,
      planFeatures,
      type: 'refresh',
    };

    const [accessToken, refreshToken] = await Promise.all([
      this.jwtService.signAsync(accessPayload, {
        expiresIn: this.configService.get('JWT_ACCESS_EXPIRATION', '15m'),
      }),
      this.jwtService.signAsync(refreshPayload, {
        expiresIn: this.configService.get('JWT_REFRESH_EXPIRATION', '7d'),
      }),
    ]);

    return { accessToken, refreshToken };
  }

  /**
   * Resolve organization ID for registration
   * 1. If organizationId is provided (invited user), validate and use it
   * 2. If organizationData is provided (new user), create a new organization
   * 3. Otherwise, throw an error (both are required)
   */
  private async resolveOrganizationId(
    organizationId?: string,
    organizationData?: RegisterOrganizationDto
  ): Promise<string> {
    // Case 1: Invited user - organizationId is provided
    if (organizationId) {
      this.logger.log(`Using existing organization for invited user: ${organizationId}`);

      // Validate that the organization exists
      const organization = await this.organizationRepository.findById(organizationId);

      if (!organization) {
        throw new BadRequestException('Invalid organization ID - organization does not exist');
      }

      if (!organization.isActive) {
        throw new BadRequestException('Organization is not active');
      }

      return organizationId;
    }

    // Case 2: New user - create a new organization
    if (organizationData) {
      this.logger.log(`Creating new organization for user registration: ${organizationData.name}`);

      // Check if organization name already exists
      const existingOrg = await this.organizationRepository.findByName(organizationData.name);

      if (existingOrg) {
        throw new ConflictException('An organization with this name already exists');
      }

      // Check if SCAC code already exists (for carriers)
      if (organizationData.scac) {
        const existingScac = await this.organizationRepository.findBySCAC(organizationData.scac);

        if (existingScac) {
          throw new ConflictException('An organization with this SCAC code already exists');
        }
      }

      // Create new organization
      const newOrganization = Organization.create({
        id: uuidv4(),
        name: organizationData.name,
        type: organizationData.type,
        scac: organizationData.scac,
        siren: organizationData.siren,
        siret: organizationData.siret,
        address: {
          street: organizationData.street,
          city: organizationData.city,
          state: organizationData.state,
          postalCode: organizationData.postalCode,
          country: organizationData.country,
        },
        documents: [],
        isActive: true,
      });

      const savedOrganization = await this.organizationRepository.save(newOrganization);

      this.logger.log(
        `New organization created: ${savedOrganization.id} - ${savedOrganization.name}`
      );

      return savedOrganization.id;
    }

    // Case 3: Neither provided - error
    throw new BadRequestException(
      'Either organizationId (for invited users) or organization data (for new users) must be provided'
    );
  }
}
