import {
  Controller,
  Get,
  Post,
  Delete,
  Patch,
  Put,
  Param,
  Body,
  UseGuards,
  Request,
  NotFoundException,
  Logger,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { RolesGuard, Roles } from '@it0/common';
import { TenantProvisioningService } from '@it0/database';
import { Tenant } from '../../../domain/entities/tenant.entity';
import { TenantInvite } from '../../../domain/entities/tenant-invite.entity';
import { AuthService } from '../../../application/services/auth.service';
import * as crypto from 'crypto';

@Controller('api/v1/admin/tenants')
@UseGuards(RolesGuard)
export class TenantController {
  private readonly logger = new Logger(TenantController.name);

  constructor(
    @InjectRepository(Tenant)
    private readonly tenantRepository: Repository<Tenant>,
    @InjectRepository(TenantInvite)
    private readonly inviteRepository: Repository<TenantInvite>,
    private readonly tenantProvisioningService: TenantProvisioningService,
    private readonly authService: AuthService,
    private readonly dataSource: DataSource,
  ) {}

  /* ---- Helpers ---- */

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

  private async getMemberCount(slug: string): Promise<number> {
    const schemaName = `it0_t_${slug}`;
    const qr = this.dataSource.createQueryRunner();
    await qr.connect();
    try {
      await qr.startTransaction();
      await qr.query(`SET LOCAL search_path TO "${schemaName}", public`);
      const result = await qr.query(`SELECT COUNT(*)::int AS count FROM users`);
      await qr.commitTransaction();
      return result[0]?.count ?? 0;
    } catch {
      await qr.rollbackTransaction().catch(() => {});
      return 0;
    } finally {
      await qr.release();
    }
  }

  private toDto(t: Tenant, memberCount?: number) {
    const count = memberCount ?? 0;
    return {
      id: t.id,
      name: t.name,
      slug: t.slug,
      schemaName: `it0_t_${t.slug}`,
      plan: t.plan,
      status: t.status,
      adminEmail: t.adminEmail,
      userCount: count,
      memberCount: count,
      quota: {
        maxServers: t.maxServers,
        maxUsers: t.maxUsers,
        maxStandingOrders: t.maxStandingOrders,
        maxAgentTokensPerMonth: t.maxAgentTokensPerMonth,
      },
      createdAt: t.createdAt,
      updatedAt: t.updatedAt,
    };
  }

  private async findTenantOrFail(id: string): Promise<Tenant> {
    const tenant = await this.tenantRepository.findOne({ where: { id } });
    if (!tenant) {
      throw new NotFoundException(`Tenant with id "${id}" not found`);
    }
    return tenant;
  }

  /* ---- Tenant CRUD ---- */

  /**
   * GET /api/v1/admin/tenants
   */
  @Get()
  @Roles('platform_admin', 'platform_super_admin')
  async listTenants() {
    const tenants = await this.tenantRepository.find({ order: { createdAt: 'DESC' } });
    return tenants.map((t) => this.toDto(t));
  }

  /**
   * POST /api/v1/admin/tenants
   */
  @Post()
  @Roles('platform_admin', 'platform_super_admin')
  async createTenant(
    @Body()
    body: {
      name: string;
      slug: string;
      plan?: 'free' | 'pro' | 'enterprise';
      adminEmail: string;
      maxServers?: number;
      maxUsers?: number;
      maxStandingOrders?: number;
      maxAgentTokensPerMonth?: number;
    },
  ) {
    const tenant = this.tenantRepository.create({
      id: crypto.randomUUID(),
      name: body.name,
      slug: body.slug,
      plan: body.plan ?? 'free',
      status: 'active',
      adminEmail: body.adminEmail,
      maxServers: body.maxServers ?? 10,
      maxUsers: body.maxUsers ?? 5,
      maxStandingOrders: body.maxStandingOrders ?? 20,
      maxAgentTokensPerMonth: body.maxAgentTokensPerMonth ?? 100000,
    });

    const savedTenant = await this.tenantRepository.save(tenant);

    try {
      await this.tenantProvisioningService.provisionTenant(savedTenant.slug);
      this.logger.log(`Schema provisioned for tenant "${savedTenant.slug}"`);
    } catch (err) {
      this.logger.error(
        `Failed to provision schema for tenant "${savedTenant.slug}":`,
        err,
      );
    }

    return this.toDto(savedTenant);
  }

  /* ---- Member Management ---- */

  /**
   * GET /api/v1/admin/tenants/:id/members
   */
  @Get(':id/members')
  @Roles('admin', 'platform_admin', 'platform_super_admin')
  async listMembers(@Param('id') id: string) {
    const tenant = await this.findTenantOrFail(id);
    const schemaName = `it0_t_${tenant.slug}`;
    const qr = this.dataSource.createQueryRunner();
    await qr.connect();
    try {
      await qr.query(`SET search_path TO "${schemaName}", public`);
      const rows = await qr.query(
        `SELECT id, email, name, roles, is_active, created_at FROM users ORDER BY created_at ASC`,
      );
      return rows.map((row: any) => ({
        id: row.id,
        email: row.email,
        name: row.name,
        role: this.parseRole(row.roles),
        joinedAt: row.created_at,
      }));
    } finally {
      await qr.release();
    }
  }

  /**
   * PATCH /api/v1/admin/tenants/:id/members/:memberId
   */
  @Patch(':id/members/:memberId')
  @Roles('admin', 'platform_admin', 'platform_super_admin')
  async updateMember(
    @Param('id') tenantId: string,
    @Param('memberId') memberId: string,
    @Body() body: { role?: string; status?: string },
  ) {
    const tenant = await this.findTenantOrFail(tenantId);
    const schemaName = `it0_t_${tenant.slug}`;
    const qr = this.dataSource.createQueryRunner();
    await qr.connect();
    try {
      await qr.query(`SET search_path TO "${schemaName}", public`);

      const existing = await qr.query(
        `SELECT id FROM users WHERE id = $1`,
        [memberId],
      );
      if (existing.length === 0) {
        throw new NotFoundException(`Member "${memberId}" not found`);
      }

      const updates: string[] = [];
      const params: any[] = [];
      let idx = 1;

      if (body.role) {
        updates.push(`roles = $${idx}`);
        params.push([body.role]);
        idx++;
      }
      if (body.status !== undefined) {
        updates.push(`is_active = $${idx}`);
        params.push(body.status === 'active');
        idx++;
      }

      if (updates.length > 0) {
        updates.push('updated_at = NOW()');
        params.push(memberId);
        await qr.query(
          `UPDATE users SET ${updates.join(', ')} WHERE id = $${idx}`,
          params,
        );
        // Sync role change to public.users so it takes effect on next login
        if (body.role) {
          await this.dataSource.query(
            `UPDATE public.users SET roles = $1, updated_at = NOW() WHERE id = $2`,
            [[body.role], memberId],
          );
        }
      }

      const updated = await qr.query(
        `SELECT id, email, name, roles, is_active, created_at FROM users WHERE id = $1`,
        [memberId],
      );
      const row = updated[0];
      return {
        id: row.id,
        email: row.email,
        name: row.name,
        role: this.parseRole(row.roles),
        joinedAt: row.created_at,
      };
    } finally {
      await qr.release();
    }
  }

  /**
   * DELETE /api/v1/admin/tenants/:id/members/:memberId
   */
  @Delete(':id/members/:memberId')
  @Roles('admin', 'platform_admin', 'platform_super_admin')
  async removeMember(
    @Param('id') tenantId: string,
    @Param('memberId') memberId: string,
  ) {
    const tenant = await this.findTenantOrFail(tenantId);
    const schemaName = `it0_t_${tenant.slug}`;
    const qr = this.dataSource.createQueryRunner();
    await qr.connect();
    try {
      await qr.query(`SET search_path TO "${schemaName}", public`);

      const existing = await qr.query(
        `SELECT id FROM users WHERE id = $1`,
        [memberId],
      );
      if (existing.length === 0) {
        throw new NotFoundException(`Member "${memberId}" not found`);
      }

      await qr.query(`DELETE FROM users WHERE id = $1`, [memberId]);
      // Also remove from global user index so the user can no longer log in
      await this.dataSource.query(
        `DELETE FROM public.users WHERE id = $1`, [memberId],
      );
      return { success: true };
    } finally {
      await qr.release();
    }
  }

  /* ---- Invitation Management ---- */

  /**
   * GET /api/v1/admin/tenants/:id/invites
   */
  @Get(':id/invites')
  @Roles('admin', 'platform_admin', 'platform_super_admin')
  async listInvites(@Param('id') id: string) {
    const tenant = await this.findTenantOrFail(id);
    const invites = await this.authService.listInvites(tenant.slug);
    return invites.map((inv) => ({
      id: inv.id,
      email: inv.email,
      role: inv.role,
      status: inv.status,
      expiresAt: inv.expiresAt,
      invitedBy: inv.invitedBy,
      createdAt: inv.createdAt,
    }));
  }

  /**
   * POST /api/v1/admin/tenants/:id/invites
   */
  @Post(':id/invites')
  @Roles('admin', 'platform_admin', 'platform_super_admin')
  async createInvite(
    @Param('id') id: string,
    @Body() body: { email: string; role?: string },
    @Request() req: any,
  ) {
    const tenant = await this.findTenantOrFail(id);
    const invite = await this.authService.createInvite(
      tenant.slug,
      body.email,
      body.role || 'viewer',
      req.user?.sub || 'system',
    );
    return {
      id: invite.id,
      email: invite.email,
      role: invite.role,
      token: invite.token,
      status: invite.status,
      expiresAt: invite.expiresAt,
      createdAt: invite.createdAt,
    };
  }

  /**
   * DELETE /api/v1/admin/tenants/:id/invites/:inviteId
   */
  @Delete(':id/invites/:inviteId')
  @Roles('admin', 'platform_admin', 'platform_super_admin')
  async revokeInvite(
    @Param('id') id: string,
    @Param('inviteId') inviteId: string,
  ) {
    const tenant = await this.findTenantOrFail(id);
    await this.authService.revokeInvite(inviteId, tenant.slug);
    return { success: true };
  }

  /* ---- Single Tenant Operations (less-specific routes last) ---- */

  /**
   * GET /api/v1/admin/tenants/:id
   */
  @Get(':id')
  @Roles('platform_admin', 'platform_super_admin')
  async getTenant(@Param('id') id: string) {
    const tenant = await this.findTenantOrFail(id);
    const memberCount = await this.getMemberCount(tenant.slug);
    return this.toDto(tenant, memberCount);
  }

  /**
   * PATCH /api/v1/admin/tenants/:id
   */
  @Patch(':id')
  @Roles('platform_admin', 'platform_super_admin')
  async updateTenant(@Param('id') id: string, @Body() body: any) {
    const tenant = await this.findTenantOrFail(id);

    if (body.name !== undefined) tenant.name = body.name;
    if (body.plan !== undefined) tenant.plan = body.plan;
    if (body.status !== undefined) tenant.status = body.status;
    if (body.adminEmail !== undefined) tenant.adminEmail = body.adminEmail;

    // Support nested quota object from frontend
    if (body.quota) {
      if (body.quota.maxServers !== undefined) tenant.maxServers = body.quota.maxServers;
      if (body.quota.maxUsers !== undefined) tenant.maxUsers = body.quota.maxUsers;
      if (body.quota.maxStandingOrders !== undefined) tenant.maxStandingOrders = body.quota.maxStandingOrders;
      if (body.quota.maxAgentTokensPerMonth !== undefined) tenant.maxAgentTokensPerMonth = body.quota.maxAgentTokensPerMonth;
    }

    // Also support flat fields
    if (body.maxServers !== undefined) tenant.maxServers = body.maxServers;
    if (body.maxUsers !== undefined) tenant.maxUsers = body.maxUsers;
    if (body.maxStandingOrders !== undefined) tenant.maxStandingOrders = body.maxStandingOrders;
    if (body.maxAgentTokensPerMonth !== undefined) tenant.maxAgentTokensPerMonth = body.maxAgentTokensPerMonth;

    const saved = await this.tenantRepository.save(tenant);
    return this.toDto(saved);
  }

  /**
   * PUT /api/v1/admin/tenants/:id (alias for PATCH — frontend detail page uses PUT)
   */
  @Put(':id')
  @Roles('platform_admin', 'platform_super_admin')
  async updateTenantPut(@Param('id') id: string, @Body() body: any) {
    return this.updateTenant(id, body);
  }

  /**
   * DELETE /api/v1/admin/tenants/:id
   */
  @Delete(':id')
  @Roles('platform_admin', 'platform_super_admin')
  async deleteTenant(@Param('id') id: string) {
    const tenant = await this.findTenantOrFail(id);

    // 1. Drop the tenant schema (all data CASCADE)
    try {
      await this.tenantProvisioningService.deprovisionTenant(tenant.slug);
      this.logger.log(`Schema deprovisioned for tenant "${tenant.slug}"`);
    } catch (err) {
      this.logger.error(`Failed to deprovision schema for "${tenant.slug}":`, err);
    }

    // 2. Delete related records (use slug — invite.tenantId stores slug)
    await this.inviteRepository.delete({ tenantId: tenant.slug });
    await this.dataSource.query(
      `DELETE FROM public.users WHERE tenant_id = $1`, [tenant.slug],
    );
    await this.dataSource.query(
      `DELETE FROM public.api_keys WHERE tenant_id = $1`, [tenant.slug],
    );

    // 3. Delete the tenant record
    await this.tenantRepository.remove(tenant);

    return { success: true };
  }
}
