import {
  Injectable,
  UnauthorizedException,
  ConflictException,
  BadRequestException,
  NotFoundException,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import * as bcrypt from 'bcryptjs';
import * as crypto from 'crypto';
import { TenantProvisioningService } from '@it0/database';
import { UserRepository } from '../../infrastructure/repositories/user.repository';
import { ApiKeyRepository } from '../../infrastructure/repositories/api-key.repository';
import { User } from '../../domain/entities/user.entity';
import { ApiKey } from '../../domain/entities/api-key.entity';
import { Tenant } from '../../domain/entities/tenant.entity';
import { TenantInvite } from '../../domain/entities/tenant-invite.entity';
import { RoleType } from '../../domain/value-objects/role-type.vo';
import { JwtPayload } from '../../infrastructure/strategies/jwt.strategy';

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

  constructor(
    private readonly userRepository: UserRepository,
    private readonly apiKeyRepository: ApiKeyRepository,
    private readonly jwtService: JwtService,
    private readonly configService: ConfigService,
    @InjectRepository(Tenant)
    private readonly tenantRepository: Repository<Tenant>,
    @InjectRepository(TenantInvite)
    private readonly inviteRepository: Repository<TenantInvite>,
    private readonly tenantProvisioningService: TenantProvisioningService,
    private readonly dataSource: DataSource,
  ) {
    this.refreshSecret =
      this.configService.get<string>('JWT_REFRESH_SECRET') ||
      'dev-refresh-secret';
  }

  async login(
    email: string,
    password: string,
  ): Promise<{
    accessToken: string;
    refreshToken: string;
    user: { id: string; email: string; name: string; roles: string[]; tenantId: string };
  }> {
    const user = await this.userRepository.findByEmail(email);
    if (!user || !user.isActive) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const passwordValid = await bcrypt.compare(password, user.passwordHash);
    if (!passwordValid) {
      throw new UnauthorizedException('Invalid credentials');
    }

    user.lastLoginAt = new Date();
    await this.userRepository.save(user);

    const tokens = this.generateTokens(user);

    return {
      ...tokens,
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        roles: user.roles,
        tenantId: user.tenantId,
      },
    };
  }

  /**
   * Register a new user.
   * If companyName is provided, creates a new tenant with schema provisioning
   * and sets the user as admin (self-service registration).
   * Otherwise joins the default tenant as viewer.
   */
  async register(
    email: string,
    password: string,
    name: string,
    companyName?: string,
  ): Promise<{
    accessToken: string;
    refreshToken: string;
    user: { id: string; email: string; name: string; roles: string[]; tenantId: string };
  }> {
    // Check email uniqueness across all schemas (public users table check)
    const existing = await this.userRepository.findByEmail(email);
    if (existing) {
      throw new ConflictException('Email already registered');
    }

    const passwordHash = await bcrypt.hash(password, 12);

    if (companyName) {
      // Self-service: create tenant + provision schema + create admin user
      return this.registerWithNewTenant(email, passwordHash, name, companyName);
    }

    // Default: join default tenant as viewer
    const user = new User();
    user.id = crypto.randomUUID();
    user.tenantId = 'default';
    user.email = email;
    user.passwordHash = passwordHash;
    user.name = name;
    user.roles = [RoleType.VIEWER];
    user.isActive = true;
    user.createdAt = new Date();
    user.updatedAt = new Date();

    await this.userRepository.save(user);

    const tokens = this.generateTokens(user);
    return {
      ...tokens,
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        roles: user.roles,
        tenantId: user.tenantId,
      },
    };
  }

  private async registerWithNewTenant(
    email: string,
    passwordHash: string,
    name: string,
    companyName: string,
  ) {
    // Generate slug from company name (underscores for valid PostgreSQL schema names)
    const slug = companyName
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '_')
      .replace(/(^_|_$)/g, '')
      .slice(0, 40);

    // Check slug uniqueness
    const existingTenant = await this.tenantRepository.findOneBy({ slug });
    if (existingTenant) {
      throw new ConflictException('Organization name already taken');
    }

    // 1. Create tenant record in public schema
    const tenant = this.tenantRepository.create({
      id: crypto.randomUUID(),
      name: companyName,
      slug,
      plan: 'free',
      status: 'active',
      adminEmail: email,
      maxServers: 10,
      maxUsers: 5,
      maxStandingOrders: 20,
      maxAgentTokensPerMonth: 100000,
    });
    await this.tenantRepository.save(tenant);

    // 2. Provision the tenant's database schema
    try {
      await this.tenantProvisioningService.provisionTenant(slug);
      this.logger.log(`Schema provisioned for self-service tenant "${slug}"`);
    } catch (err) {
      // Rollback: remove tenant record if schema provision fails
      await this.tenantRepository.remove(tenant);
      this.logger.error(`Failed to provision schema for "${slug}":`, err);
      throw new BadRequestException('Failed to create organization. Please try again.');
    }

    // 3. Insert admin user directly into the new tenant schema
    const userId = crypto.randomUUID();
    const now = new Date();
    const schemaName = `it0_t_${slug}`;

    const qr = this.dataSource.createQueryRunner();
    await qr.connect();
    try {
      await qr.query(`SET search_path TO "${schemaName}", public`);
      await qr.query(
        `INSERT INTO users (id, tenant_id, email, password_hash, name, roles, is_active, created_at, updated_at)
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
        [userId, slug, email, passwordHash, name, [RoleType.ADMIN], true, now, now],
      );
    } finally {
      await qr.release();
    }

    // Build user object for token generation
    const user = new User();
    user.id = userId;
    user.tenantId = slug;
    user.email = email;
    user.passwordHash = passwordHash;
    user.name = name;
    user.roles = [RoleType.ADMIN];
    user.isActive = true;

    const tokens = this.generateTokens(user);
    return {
      ...tokens,
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        roles: user.roles,
        tenantId: user.tenantId,
      },
    };
  }

  /* ---- Invitation Flow ---- */

  async createInvite(
    tenantId: string,
    email: string,
    role: string,
    invitedBy: string,
  ): Promise<TenantInvite> {
    // Check tenant exists
    const tenant = await this.tenantRepository.findOneBy({ id: tenantId });
    if (!tenant) {
      throw new NotFoundException('Tenant not found');
    }

    // Check user quota
    if (tenant.maxUsers !== -1) {
      const schemaName = `it0_t_${tenantId}`;
      const countResult = await this.dataSource.query(
        `SELECT COUNT(*) AS cnt FROM "${schemaName}".users WHERE is_active = true`,
      );
      const currentCount = Number(countResult[0]?.cnt ?? 0);
      if (currentCount >= tenant.maxUsers) {
        throw new HttpException(
          { message: `User quota exceeded (${currentCount}/${tenant.maxUsers}). Upgrade your plan to invite more users.`, code: 'QUOTA_EXCEEDED' },
          HttpStatus.TOO_MANY_REQUESTS,
        );
      }
    }

    // Check for existing pending invite
    const existing = await this.inviteRepository.findOne({
      where: { tenantId, email, status: 'pending' as const },
    });
    if (existing) {
      throw new ConflictException('An invitation has already been sent to this email');
    }

    const invite = this.inviteRepository.create({
      id: crypto.randomUUID(),
      tenantId,
      email,
      token: crypto.randomBytes(32).toString('hex'),
      role: role || RoleType.VIEWER,
      status: 'pending',
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
      invitedBy,
    });

    return this.inviteRepository.save(invite);
  }

  async listInvites(tenantId: string): Promise<TenantInvite[]> {
    return this.inviteRepository.find({
      where: { tenantId },
      order: { createdAt: 'DESC' },
    });
  }

  async revokeInvite(inviteId: string, tenantId: string): Promise<void> {
    const invite = await this.inviteRepository.findOne({
      where: { id: inviteId, tenantId },
    });
    if (!invite) {
      throw new NotFoundException('Invite not found');
    }
    invite.status = 'revoked';
    await this.inviteRepository.save(invite);
  }

  async validateInvite(token: string): Promise<{
    email: string;
    tenantName: string;
    role: string;
    expiresAt: string;
  }> {
    const invite = await this.inviteRepository.findOneBy({ token });
    if (!invite) {
      throw new NotFoundException('Invalid invitation link');
    }
    if (invite.status !== 'pending') {
      throw new BadRequestException(`Invitation has been ${invite.status}`);
    }
    if (new Date() > invite.expiresAt) {
      invite.status = 'expired';
      await this.inviteRepository.save(invite);
      throw new BadRequestException('Invitation has expired');
    }

    const tenant = await this.tenantRepository.findOneBy({ id: invite.tenantId });
    return {
      email: invite.email,
      tenantName: tenant?.name || 'Unknown',
      role: invite.role,
      expiresAt: invite.expiresAt.toISOString(),
    };
  }

  async acceptInvite(
    token: string,
    password: string,
    name: string,
  ): Promise<{
    accessToken: string;
    refreshToken: string;
    user: { id: string; email: string; name: string; roles: string[]; tenantId: string };
  }> {
    const invite = await this.inviteRepository.findOneBy({ token });
    if (!invite || invite.status !== 'pending') {
      throw new BadRequestException('Invalid or expired invitation');
    }
    if (new Date() > invite.expiresAt) {
      invite.status = 'expired';
      await this.inviteRepository.save(invite);
      throw new BadRequestException('Invitation has expired');
    }

    // Find the tenant to get the slug (used as schema name)
    const tenant = await this.tenantRepository.findOneBy({ id: invite.tenantId });
    if (!tenant || tenant.status !== 'active') {
      throw new BadRequestException('Tenant is not active');
    }

    const passwordHash = await bcrypt.hash(password, 12);
    const userId = crypto.randomUUID();
    const now = new Date();
    const schemaName = `it0_t_${tenant.slug}`;

    // Insert user into the tenant's schema
    const qr = this.dataSource.createQueryRunner();
    await qr.connect();
    try {
      await qr.query(`SET search_path TO "${schemaName}", public`);

      // Check if email already exists in this tenant
      const existingRows = await qr.query(
        `SELECT id FROM users WHERE email = $1 LIMIT 1`,
        [invite.email],
      );
      if (existingRows.length > 0) {
        throw new ConflictException('Email already registered in this organization');
      }

      await qr.query(
        `INSERT INTO users (id, tenant_id, email, password_hash, name, roles, is_active, created_at, updated_at)
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
        [userId, tenant.slug, invite.email, passwordHash, name, [invite.role], true, now, now],
      );
    } finally {
      await qr.release();
    }

    // Mark invite as accepted
    invite.status = 'accepted';
    await this.inviteRepository.save(invite);

    // Build user object for token generation
    const user = new User();
    user.id = userId;
    user.tenantId = tenant.slug;
    user.email = invite.email;
    user.passwordHash = passwordHash;
    user.name = name;
    user.roles = [invite.role];
    user.isActive = true;

    const tokens = this.generateTokens(user);
    return {
      ...tokens,
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        roles: user.roles,
        tenantId: user.tenantId,
      },
    };
  }

  async refreshToken(
    refreshToken: string,
  ): Promise<{ accessToken: string; refreshToken: string }> {
    let payload: any;
    try {
      payload = this.jwtService.verify(refreshToken, {
        secret: this.refreshSecret,
      });
    } catch {
      throw new UnauthorizedException('Invalid refresh token');
    }

    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');
    }

    return this.generateTokens(user);
  }

  async createApiKey(
    userId: string,
    tenantId: string,
    name: string,
  ): Promise<{ id: string; key: string; name: string }> {
    const rawKey = crypto.randomBytes(32).toString('hex');
    const keyHash = crypto
      .createHash('sha256')
      .update(rawKey)
      .digest('hex');

    const apiKey = new ApiKey();
    apiKey.id = crypto.randomUUID();
    apiKey.tenantId = tenantId;
    apiKey.userId = userId;
    apiKey.keyHash = keyHash;
    apiKey.name = name;
    apiKey.permissions = [];
    apiKey.isActive = true;
    apiKey.createdAt = new Date();

    await this.apiKeyRepository.save(apiKey);

    return { id: apiKey.id, key: rawKey, name: apiKey.name };
  }

  private generateTokens(user: User): {
    accessToken: string;
    refreshToken: string;
  } {
    const jwtPayload: JwtPayload = {
      sub: user.id,
      email: user.email,
      tenantId: user.tenantId,
      roles: user.roles,
    };

    const accessToken = this.jwtService.sign({
      ...jwtPayload,
      kid: 'it0-auth',
    });

    const refreshToken = this.jwtService.sign(
      { sub: user.id, type: 'refresh' },
      { secret: this.refreshSecret, expiresIn: '7d' },
    );

    return { accessToken, refreshToken };
  }
}
