import {
  Body,
  Controller,
  Delete,
  ForbiddenException,
  Get,
  HttpCode,
  Param,
  Patch,
  Post,
  Put,
  Query,
  Req,
  UseGuards,
} from '@nestjs/common'
import { ActorService } from '../auth/actor.service.js'
import { clientIp } from '../auth/client-ip.js'
import { CurrentUser } from '../auth/current-user.decorator.js'
import { JwtAuthGuard } from '../auth/jwt-auth.guard.js'
import { OperatorGuard } from '../auth/operator.guard.js'
import type { AuthentikJwtPayload } from '../auth/jwt-payload.interface.js'
import { AuditService, type AuditActor } from '../audit/audit.service.js'
import { CreateSsoAppDto } from './dto/create-sso-app.dto.js'
import { CreateTenantDto } from './dto/create-tenant.dto.js'
import { UpdateBillingInfoDto } from './dto/update-billing-info.dto.js'
import { UpdateSecurityPolicyDto } from './dto/update-security-policy.dto.js'
import { UpdateTenantBrandingDto } from './dto/update-tenant-branding.dto.js'
import { UpdateTenantDto } from './dto/update-tenant.dto.js'
import { StorageService } from './storage.service.js'
import { TenantBrandingService } from './tenant-branding.service.js'
import { roleForTenant } from '../schemas/user.schema.js'
import { TenantSsoService } from './tenant-sso.service.js'
import { TenantsService } from './tenants.service.js'

// Build the audit actor from a resolved User doc + the originating request.
// Threaded into every mutation so the audit log records who did it from where.
function auditActor(
  user: { _id: unknown; email: string },
  req: Parameters<typeof clientIp>[0],
): AuditActor {
  return {
    userId: String(user._id),
    email: user.email,
    ip: clientIp(req),
  }
}

@Controller('tenants')
@UseGuards(JwtAuthGuard)
export class TenantsController {
  constructor(
    private readonly tenants: TenantsService,
    private readonly actor: ActorService,
    private readonly audit: AuditService,
    private readonly branding: TenantBrandingService,
    private readonly storage: StorageService,
    private readonly sso: TenantSsoService,
  ) {}

  @Post()
  async create(@Body() dto: CreateTenantDto, @CurrentUser() jwt: AuthentikJwtPayload, @Req() req: Parameters<typeof clientIp>[0]) {
    const user = await this.actor.resolve(jwt)
    if (!user.platformAdmin) {
      throw new ForbiddenException('Only platform admins can create tenants')
    }
    return this.tenants.create(dto, auditActor(user, req))
  }

  @Get()
  async findAll(@CurrentUser() jwt: AuthentikJwtPayload) {
    const actor = await this.actor.resolve(jwt)
    if (actor.platformAdmin) return this.tenants.findAll()
    return this.tenants.findByIds(actor.tenantIds)
  }

  @Get(':slug')
  async findOne(@Param('slug') slug: string, @CurrentUser() jwt: AuthentikJwtPayload) {
    const actor = await this.actor.resolve(jwt)
    const tenant = await this.tenants.findOneBySlug(slug)
    if (!actor.platformAdmin && !actor.tenantIds.some((id) => id.equals(tenant._id))) {
      throw new ForbiddenException(`No access to tenant "${slug}"`)
    }
    return tenant
  }

  @Get(':slug/users')
  async listUsers(@Param('slug') slug: string, @CurrentUser() jwt: AuthentikJwtPayload) {
    const actor = await this.actor.resolve(jwt)
    const tenant = await this.tenants.findOneBySlug(slug)
    if (!actor.platformAdmin && !actor.tenantIds.some((id) => id.equals(tenant._id))) {
      throw new ForbiddenException(`No access to tenant "${slug}"`)
    }
    // Add a tenant-scoped `tenantRole` to each user so the UI shows the role
    // for THIS tenant, not the global fallback. `role` is left intact for any
    // other consumer; resolution stays server-side so the precedence in
    // roleForTenant() isn't duplicated per frontend.
    const users = await this.tenants.listUsersForTenant(slug)
    return users.map((u) => ({
      ...u.toObject(),
      tenantRole: roleForTenant(u, tenant._id),
    }))
  }

  // Aggregate storage usage for the customer-admin Storage page. Same membership
  // gate as GET :slug/users — any member of the tenant can read their own
  // workspace's storage. Computed live from OCIS libregraph (read-only); if OCIS
  // is unreachable the summary comes back with available=false.
  @Get(':slug/storage')
  async getStorage(@Param('slug') slug: string, @CurrentUser() jwt: AuthentikJwtPayload) {
    const actor = await this.actor.resolve(jwt)
    const tenant = await this.tenants.findOneBySlug(slug)
    if (!actor.platformAdmin && !actor.tenantIds.some((id) => id.equals(tenant._id))) {
      throw new ForbiddenException(`No access to tenant "${slug}"`)
    }
    return this.storage.getStorageSummary(slug)
  }

  // Tenant-scoped audit slice for the customer-admin dashboard. Same membership
  // gate as GET :slug — any member of the tenant can read their own workspace's
  // activity. This is the portal-accessible counterpart to the operator-only
  // GET /audit (which requires dezky-operator audience). Filtered strictly by
  // tenantSlug so a caller only ever sees their own tenant's events.
  @Get(':slug/audit')
  async listAudit(
    @Param('slug') slug: string,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Query('limit') limit?: string,
    @Query('q') q?: string,
    @Query('action') action?: string,
    @Query('outcome') outcome?: string,
    @Query('actorEmail') actorEmail?: string,
    @Query('since') since?: string,
    @Query('before') before?: string,
  ) {
    const actor = await this.actor.resolve(jwt)
    const tenant = await this.tenants.findOneBySlug(slug)
    if (!actor.platformAdmin && !actor.tenantIds.some((id) => id.equals(tenant._id))) {
      throw new ForbiddenException(`No access to tenant "${slug}"`)
    }
    const parsed = limit ? Number.parseInt(limit, 10) : undefined
    // Always pinned to this tenant's slug — the filters only ever narrow within
    // it, never widen across tenants.
    return this.audit.list({
      tenantSlug: slug,
      limit: Number.isFinite(parsed) ? parsed : undefined,
      q: q || undefined,
      action: action || undefined,
      outcome: outcome === 'success' || outcome === 'failure' ? outcome : undefined,
      actorEmail: actorEmail || undefined,
      since: since ? new Date(since) : undefined,
      before: before ? new Date(before) : undefined,
    })
  }

  @Patch(':slug')
  async update(
    @Param('slug') slug: string,
    @Body() dto: UpdateTenantDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const user = await this.actor.resolve(jwt)
    const tenant = await this.tenants.findOneBySlug(slug)
    if (!user.platformAdmin && !user.tenantIds.some((id) => id.equals(tenant._id))) {
      throw new ForbiddenException(`No access to tenant "${slug}"`)
    }
    return this.tenants.update(slug, dto, auditActor(user, req))
  }

  // Customer admins can fix their own company/tax details. Narrow on purpose —
  // only billingInfo, never plan/status/partner (unlike the broad PATCH above).
  // Membership-gated like the other tenant-scoped portal reads.
  @Patch(':slug/billing-info')
  async updateBillingInfo(
    @Param('slug') slug: string,
    @Body() dto: UpdateBillingInfoDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const user = await this.actor.resolve(jwt)
    const tenant = await this.tenants.findOneBySlug(slug)
    if (!user.platformAdmin && !user.tenantIds.some((id) => id.equals(tenant._id))) {
      throw new ForbiddenException(`No access to tenant "${slug}"`)
    }
    return this.tenants.updateBillingInfo(slug, dto, auditActor(user, req))
  }

  // Whitelabel branding (name + accent + email-template overrides). Narrow,
  // membership-gated — never touches plan/status/partner.
  @Get(':slug/branding')
  async getBranding(@Param('slug') slug: string, @CurrentUser() jwt: AuthentikJwtPayload) {
    const user = await this.actor.resolve(jwt)
    const tenant = await this.tenants.findOneBySlug(slug)
    if (!user.platformAdmin && !user.tenantIds.some((id) => id.equals(tenant._id))) {
      throw new ForbiddenException(`No access to tenant "${slug}"`)
    }
    return this.branding.get(tenant)
  }

  @Put(':slug/branding')
  async putBranding(
    @Param('slug') slug: string,
    @Body() dto: UpdateTenantBrandingDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const user = await this.actor.resolve(jwt)
    const tenant = await this.tenants.findOneBySlug(slug)
    if (!user.platformAdmin && !user.tenantIds.some((id) => id.equals(tenant._id))) {
      throw new ForbiddenException(`No access to tenant "${slug}"`)
    }
    return this.branding.put(tenant, dto, auditActor(user, req))
  }

  // Security policy (stored intent; enforcement wired incrementally).
  @Patch(':slug/security-policy')
  async updateSecurityPolicy(
    @Param('slug') slug: string,
    @Body() dto: UpdateSecurityPolicyDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const user = await this.actor.resolve(jwt)
    const tenant = await this.tenants.findOneBySlug(slug)
    if (!user.platformAdmin && !user.tenantIds.some((id) => id.equals(tenant._id))) {
      throw new ForbiddenException(`No access to tenant "${slug}"`)
    }
    return this.tenants.updateSecurityPolicy(slug, dto, auditActor(user, req))
  }

  // Live MFA-enrollment overview for the workspace.
  @Get(':slug/mfa-status')
  async mfaStatus(@Param('slug') slug: string, @CurrentUser() jwt: AuthentikJwtPayload) {
    const user = await this.actor.resolve(jwt)
    const tenant = await this.tenants.findOneBySlug(slug)
    if (!user.platformAdmin && !user.tenantIds.some((id) => id.equals(tenant._id))) {
      throw new ForbiddenException(`No access to tenant "${slug}"`)
    }
    return this.tenants.mfaStatusForTenant(slug)
  }

  // SSO apps — Dezky as IdP. Creates real Authentik OAuth2 providers +
  // applications scoped to the tenant group. Membership-gated.
  @Get(':slug/sso-apps')
  async listSsoApps(@Param('slug') slug: string, @CurrentUser() jwt: AuthentikJwtPayload) {
    const user = await this.actor.resolve(jwt)
    const tenant = await this.tenants.findOneBySlug(slug)
    if (!user.platformAdmin && !user.tenantIds.some((id) => id.equals(tenant._id))) {
      throw new ForbiddenException(`No access to tenant "${slug}"`)
    }
    return this.sso.list(tenant)
  }

  @Post(':slug/sso-apps')
  async createSsoApp(
    @Param('slug') slug: string,
    @Body() dto: CreateSsoAppDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const user = await this.actor.resolve(jwt)
    const tenant = await this.tenants.findOneBySlug(slug)
    if (!user.platformAdmin && !user.tenantIds.some((id) => id.equals(tenant._id))) {
      throw new ForbiddenException(`No access to tenant "${slug}"`)
    }
    return this.sso.create(tenant, dto, auditActor(user, req))
  }

  @Delete(':slug/sso-apps/:id')
  @HttpCode(204)
  async deleteSsoApp(
    @Param('slug') slug: string,
    @Param('id') id: string,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const user = await this.actor.resolve(jwt)
    const tenant = await this.tenants.findOneBySlug(slug)
    if (!user.platformAdmin && !user.tenantIds.some((id) => id.equals(tenant._id))) {
      throw new ForbiddenException(`No access to tenant "${slug}"`)
    }
    await this.sso.remove(tenant, id, auditActor(user, req))
  }

  @Delete(':slug')
  @HttpCode(204)
  async remove(@Param('slug') slug: string, @CurrentUser() jwt: AuthentikJwtPayload, @Req() req: Parameters<typeof clientIp>[0]) {
    const user = await this.actor.resolve(jwt)
    if (!user.platformAdmin) {
      throw new ForbiddenException('Only platform admins can delete tenants')
    }
    await this.tenants.softDelete(slug, auditActor(user, req))
  }

  // Manually re-run provisioning. Useful when an integration was down at create
  // time, or when external state drifted (someone deleted the Authentik group
  // out of band). Idempotent — already-OK steps no-op.
  @Post(':slug/reconcile')
  async reconcile(@Param('slug') slug: string, @CurrentUser() jwt: AuthentikJwtPayload) {
    const actor = await this.actor.resolve(jwt)
    const tenant = await this.tenants.findOneBySlug(slug)
    if (!actor.platformAdmin && !actor.tenantIds.some((id) => id.equals(tenant._id))) {
      throw new ForbiddenException(`No access to tenant "${slug}"`)
    }
    return this.tenants.reconcile(slug)
  }

  // Lifecycle: operator-only. JwtAuthGuard validates the token first, then
  // OperatorGuard requires audience='dezky-operator' AND platformAdmin=true.
  @Post(':slug/suspend')
  @UseGuards(OperatorGuard)
  async suspend(@Param('slug') slug: string, @CurrentUser() jwt: AuthentikJwtPayload, @Req() req: Parameters<typeof clientIp>[0]) {
    const user = await this.actor.resolve(jwt)
    return this.tenants.setStatus(slug, 'suspended', auditActor(user, req))
  }

  @Post(':slug/resume')
  @UseGuards(OperatorGuard)
  async resume(@Param('slug') slug: string, @CurrentUser() jwt: AuthentikJwtPayload, @Req() req: Parameters<typeof clientIp>[0]) {
    const user = await this.actor.resolve(jwt)
    return this.tenants.setStatus(slug, 'active', auditActor(user, req))
  }
}
