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(
    identifier: string,
    password: string,
  ): Promise<{
    accessToken: string;
    refreshToken: string;
    user: { id: string; email?: string; phone?: string; name: string; roles: string[]; tenantId: string };
  }> {
    // Always query public.users explicitly — avoids search_path contamination
    const isPhone = /^[+\d][\d\s\-().]{6,}$/.test(identifier);
    const rows = isPhone
      ? await this.dataSource.query(
          `SELECT * FROM public.users WHERE phone = $1 AND is_active = true LIMIT 1`, [identifier],
        )
      : await this.dataSource.query(
          `SELECT * FROM public.users WHERE email = $1 AND is_active = true LIMIT 1`, [identifier],
        );
    if (!rows.length) {
      // fallback: if phone not found try email and vice versa
      const fallbackRows = isPhone
        ? await this.dataSource.query(
            `SELECT * FROM public.users WHERE email = $1 AND is_active = true LIMIT 1`, [identifier],
          )
        : await this.dataSource.query(
            `SELECT * FROM public.users WHERE phone = $1 AND is_active = true LIMIT 1`, [identifier],
          );
      if (!fallbackRows.length) throw new UnauthorizedException('Invalid credentials');
      rows.push(...fallbackRows);
    }
    const user = this.mapRow(rows[0]);
    if (!user.isActive) {
      throw new UnauthorizedException('Invalid credentials');
    }

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

    // Update last_login_at directly to avoid search_path issues
    await this.dataSource.query(
      `UPDATE public.users SET last_login_at = NOW() WHERE id = $1`, [user.id],
    );

    const tokens = this.generateTokens(user);

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

  /**
   * Login by phone number only (OTP already verified by controller).
   * The user must exist in public.users with a matching phone.
   */
  async loginByPhone(phone: string): Promise<{
    accessToken: string;
    refreshToken: string;
    user: { id: string; email?: string; phone?: string; name: string; roles: string[]; tenantId: string };
  }> {
    const rows = await this.dataSource.query(
      `SELECT * FROM public.users WHERE phone = $1 AND is_active = true LIMIT 1`, [phone],
    );
    if (!rows.length) throw new UnauthorizedException('该手机号未注册');
    const user = this.mapRow(rows[0]);
    await this.dataSource.query(
      `UPDATE public.users SET last_login_at = NOW() WHERE id = $1`, [user.id],
    );
    const tokens = this.generateTokens(user);
    return {
      ...tokens,
      user: { id: user.id, email: user.email, phone: user.phone, 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(
    password: string,
    name: string,
    companyName?: string,
    email?: string,
    phone?: string,
  ): Promise<{
    accessToken: string;
    refreshToken: string;
    user: { id: string; email?: string; phone?: string; name: string; roles: string[]; tenantId: string };
  }> {
    if (!email && !phone) {
      throw new BadRequestException('Email or phone is required');
    }

    // Check uniqueness — always query public.users explicitly to avoid search_path issues
    if (email) {
      const rows = await this.dataSource.query(
        `SELECT id FROM public.users WHERE email = $1 LIMIT 1`, [email],
      );
      if (rows.length > 0) throw new ConflictException('Email already registered');
    }
    if (phone) {
      const rows = await this.dataSource.query(
        `SELECT id FROM public.users WHERE phone = $1 LIMIT 1`, [phone],
      );
      if (rows.length > 0) throw new ConflictException('Phone number already registered');
    }

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

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

    // Default: join default tenant as viewer
    const user = new User();
    user.id = crypto.randomUUID();
    user.tenantId = 'default';
    user.email = email;
    user.phone = phone;
    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);

    // Async: register with referral-service (fire-and-forget)
    this.registerReferral(user.tenantId, user.id).catch(() => {});
    // Also create user-level personal referral code (C2C circle)
    this.registerUserReferral(user.id).catch(() => {});

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

  private async registerWithNewTenant(
    passwordHash: string,
    name: string,
    companyName: string,
    email?: string,
    phone?: string,
  ) {
    // Generate slug from company name (underscores for valid PostgreSQL schema names)
    let slug = companyName
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '_')
      .replace(/(^_|_$)/g, '')
      .slice(0, 40);
    // If company name is all non-ASCII (e.g. Chinese), slug will be empty — use UUID fallback
    if (!slug) {
      slug = crypto.randomUUID().replace(/-/g, '').slice(0, 16);
    }

    // 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. Build user object and save to public.users (enables login via findByEmail/findByPhone)
    const userId = crypto.randomUUID();
    const now = new Date();
    const schemaName = `it0_t_${slug}`;

    // 3+4. Insert into both public.users (auth lookup) AND tenant schema (management) in one QR
    const qr = this.dataSource.createQueryRunner();
    await qr.connect();
    try {
      await qr.startTransaction();

      // a. Insert into public.users — explicit schema qualifier, no search_path dependency
      await qr.query(
        `INSERT INTO public.users (id, tenant_id, email, phone, password_hash, name, roles, is_active, created_at, updated_at)
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
        [userId, slug, email ?? null, phone ?? null, passwordHash, name, [RoleType.ADMIN], true, now, now],
      );

      // b. Insert into tenant schema for tenant-context management
      await qr.query(`SET LOCAL search_path TO "${schemaName}", public`);
      await qr.query(
        `INSERT INTO users (id, tenant_id, email, phone, password_hash, name, roles, is_active, created_at, updated_at)
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
        [userId, slug, email ?? null, phone ?? null, passwordHash, name, [RoleType.ADMIN], true, now, now],
      );

      await qr.commitTransaction();
    } catch (err) {
      await qr.rollbackTransaction();
      throw err;
    } finally {
      await qr.release();
    }

    const user = new User();
    user.id = userId;
    user.tenantId = slug;
    user.email = email;
    user.phone = phone;
    user.passwordHash = passwordHash;
    user.name = name;
    user.roles = [RoleType.ADMIN];
    user.isActive = true;

    // Async: register tenant with referral-service (fire-and-forget)
    this.registerReferral(slug, userId).catch(() => {});
    // Also create user-level personal referral code (C2C circle)
    this.registerUserReferral(userId).catch(() => {});

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

  /**
   * Fire-and-forget call to referral-service to create the user's personal referral code
   * and optionally link a personal-circle referrer (C2C layer).
   * userReferralCode must start with "USR-" to distinguish from tenant codes.
   */
  private async registerUserReferral(userId: string, userReferralCode?: string): Promise<void> {
    const referralServiceUrl = this.configService.get<string>(
      'REFERRAL_SERVICE_URL',
      'http://referral-service:3012',
    );
    const internalKey = this.configService.get<string>('INTERNAL_API_KEY', 'changeme-internal-key');
    const url = `${referralServiceUrl}/api/v1/referral/internal/user-register`;
    const http = await import('http');
    const https = await import('https');
    const body = JSON.stringify({ userId, userReferralCode });
    return new Promise((resolve) => {
      const lib = url.startsWith('https') ? https : http;
      const req = lib.request(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Content-Length': Buffer.byteLength(body),
          'X-Internal-Api-Key': internalKey,
        },
      }, (res) => { res.resume(); resolve(); });
      req.on('error', () => resolve());
      req.write(body);
      req.end();
    });
  }

  /**
   * Fire-and-forget call to referral-service to create the tenant's referral code.
   * Uses HTTP to the internal referral-service URL (not through Kong).
   */
  private async registerReferral(tenantId: string, userId: string, referralCode?: string): Promise<void> {
    const referralServiceUrl = this.configService.get<string>(
      'REFERRAL_SERVICE_URL',
      'http://referral-service:3012',
    );
    const internalKey = this.configService.get<string>('INTERNAL_API_KEY', 'changeme-internal-key');
    const url = `${referralServiceUrl}/api/v1/referral/internal/register`;
    const http = await import('http');
    const https = await import('https');
    const body = JSON.stringify({ tenantId, userId, referralCode });

    return new Promise((resolve) => {
      const lib = url.startsWith('https') ? https : http;
      const req = lib.request(
        url,
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Content-Length': Buffer.byteLength(body),
            'X-Internal-Api-Key': internalKey,
          },
        },
        (res) => {
          res.resume(); // consume response body
          resolve();
        },
      );
      req.on('error', () => resolve()); // silently ignore errors
      req.write(body);
      req.end();
    });
  }

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

  async createInvite(
    tenantId: string,
    email: string,
    role: string,
    invitedBy: string,
  ): Promise<TenantInvite> {
    // Check tenant exists — tenantId here is the slug (matches user.tenantId in JWT)
    const tenant = await this.tenantRepository.findOneBy({ slug: 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({ slug: 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); invite.tenantId stores the slug
    const tenant = await this.tenantRepository.findOneBy({ slug: 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.startTransaction();
      // Check if email already exists in public.users (cross-tenant uniqueness)
      const existingPublic = await qr.query(
        `SELECT id FROM public.users WHERE email = $1 LIMIT 1`,
        [invite.email],
      );
      if (existingPublic.length > 0) {
        throw new ConflictException('Email already registered');
      }

      // a. Insert into public.users — enables login via email lookup
      await qr.query(
        `INSERT INTO public.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],
      );

      // b. Insert into tenant schema for tenant-context management
      await qr.query(`SET LOCAL 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, tenant.slug, invite.email, passwordHash, name, [invite.role], true, now, now],
      );
      await qr.commitTransaction();
    } catch (err) {
      await qr.rollbackTransaction();
      throw err;
    } 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 rows = await this.dataSource.query(
      `SELECT * FROM public.users WHERE id = $1 AND is_active = true LIMIT 1`, [payload.sub],
    );
    if (!rows.length) {
      throw new UnauthorizedException('User not found or inactive');
    }
    return this.generateTokens(this.mapRow(rows[0]));
  }

  async getMyOrg(slug: string): Promise<{
    id: string; name: string; slug: string; plan: string; status: string;
    adminEmail: string; maxUsers: number; maxServers: number;
    maxStandingOrders: number; maxAgentTokensPerMonth: number;
    members: { id: string; email?: string; phone?: string; name: string; role: string; joinedAt: string }[];
  }> {
    const rows = await this.dataSource.query(
      `SELECT id, name, slug, plan, status, admin_email, max_users, max_servers, max_standing_orders, max_agent_tokens_per_month
       FROM public.tenants WHERE slug = $1 LIMIT 1`,
      [slug],
    );
    if (!rows.length) throw new Error('Tenant not found');
    const row = rows[0];
    const schemaName = `it0_t_${slug}`;
    let members: any[] = [];
    try {
      members = await this.dataSource.query(
        `SELECT id, email, phone, name, roles, created_at FROM "${schemaName}".users WHERE is_active = true ORDER BY created_at ASC`,
      );
    } catch { /* schema may not exist yet */ }

    const parseRole = (r: any) => {
      if (Array.isArray(r)) return r[0] ?? 'viewer';
      if (typeof r === 'string' && r.startsWith('{')) return r.slice(1, -1).split(',')[0] || 'viewer';
      return 'viewer';
    };

    return {
      id: row.id,
      name: row.name,
      slug: row.slug,
      plan: row.plan,
      status: row.status,
      adminEmail: row.admin_email,
      maxUsers: row.max_users,
      maxServers: row.max_servers,
      maxStandingOrders: row.max_standing_orders,
      maxAgentTokensPerMonth: row.max_agent_tokens_per_month,
      members: members.map((m) => ({
        id: m.id,
        email: m.email ?? undefined,
        phone: m.phone ?? undefined,
        name: m.name,
        role: parseRole(m.roles),
        joinedAt: m.created_at,
      })),
    };
  }

  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,
      phone: user.phone,
      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 };
  }

  /** Map raw SQL row (snake_case) to User entity (camelCase) */
  private mapRow(raw: Record<string, any>): User {
    const u = new User();
    u.id = raw.id;
    u.tenantId = raw.tenant_id;
    u.email = raw.email ?? undefined;
    u.phone = raw.phone ?? undefined;
    u.passwordHash = raw.password_hash;
    u.name = raw.name;
    u.roles = raw.roles;
    u.isActive = raw.is_active;
    u.lastLoginAt = raw.last_login_at;
    u.createdAt = raw.created_at;
    u.updatedAt = raw.updated_at;
    return u;
  }
}
