import {
  Body,
  Controller,
  Delete,
  ForbiddenException,
  Get,
  HttpCode,
  Param,
  Patch,
  Post,
  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 type { AuthentikJwtPayload } from '../auth/jwt-payload.interface.js'
import type { AuditActor } from '../audit/audit.service.js'
import { roleForTenant } from '../schemas/user.schema.js'
import { TenantsService } from '../tenants/tenants.service.js'
import { ChangePrimaryEmailDto } from './dto/change-primary-email.dto.js'
import { CreateMailboxDto } from './dto/create-mailbox.dto.js'
import { CreateTenantMemberDto } from './dto/create-tenant-member.dto.js'
import { AddAliasDto, RemoveAliasDto } from './dto/member-alias.dto.js'
import { UpdateTenantMemberDto } from './dto/update-tenant-member.dto.js'
import { UsersService } from './users.service.js'

function auditActor(
  user: { _id: unknown; email: string },
  req: Parameters<typeof clientIp>[0],
): AuditActor {
  return { userId: String(user._id), email: user.email, ip: clientIp(req) }
}

// Create a workspace member (the Users & groups "Invite user" flow). Mounted
// under the tenant alongside GET /tenants/:slug/users (which lives in
// TenantsController); same membership gate as the other tenant-scoped resources.
// Lives in UsersModule because the cross-system provisioning is in UsersService,
// and TenantsModule can't import UsersModule (UsersModule already imports it).
@Controller('tenants/:slug/users')
@UseGuards(JwtAuthGuard)
export class TenantMembersController {
  constructor(
    private readonly users: UsersService,
    private readonly tenants: TenantsService,
    private readonly actor: ActorService,
  ) {}

  @Post()
  async create(
    @Param('slug') slug: string,
    @Body() dto: CreateTenantMemberDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    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.users.createTenantMember(
      { _id: tenant._id, slug: tenant.slug, authentikGroupId: tenant.authentikGroupId },
      dto,
      auditActor(actor, req),
    )
  }

  // Remove a member and tear down their provisioned accounts. Self-removal is
  // blocked so an admin can't lock themselves out of their own workspace.
  @Delete(':userId')
  @HttpCode(204)
  async remove(
    @Param('slug') slug: string,
    @Param('userId') userId: string,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    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}"`)
    }
    if (String(actor._id) === userId) {
      throw new ForbiddenException('You can’t remove your own account.')
    }
    await this.users.removeTenantMember(
      { _id: tenant._id, slug: tenant.slug, authentikGroupId: tenant.authentikGroupId },
      userId,
      auditActor(actor, req),
    )
  }

  // Resolve the tenant + assert the caller belongs to it (or is a platform admin).
  private async gate(slug: string, 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 { actor, tenant }
  }

  @Post(':userId/suspend')
  @HttpCode(204)
  async suspend(
    @Param('slug') slug: string,
    @Param('userId') userId: string,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const { actor, tenant } = await this.gate(slug, jwt)
    if (String(actor._id) === userId) {
      throw new ForbiddenException('You can’t suspend your own account.')
    }
    await this.users.setMemberSuspended(
      { _id: tenant._id, slug: tenant.slug },
      userId,
      true,
      auditActor(actor, req),
    )
  }

  @Post(':userId/resume')
  @HttpCode(204)
  async resume(
    @Param('slug') slug: string,
    @Param('userId') userId: string,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const { actor, tenant } = await this.gate(slug, jwt)
    await this.users.setMemberSuspended(
      { _id: tenant._id, slug: tenant.slug },
      userId,
      false,
      auditActor(actor, req),
    )
  }

  @Post(':userId/force-logout')
  async forceLogout(
    @Param('slug') slug: string,
    @Param('userId') userId: string,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const { actor, tenant } = await this.gate(slug, jwt)
    if (String(actor._id) === userId) {
      throw new ForbiddenException('You can’t force-logout your own session here.')
    }
    return this.users.forceLogoutMember(
      { _id: tenant._id, slug: tenant.slug },
      userId,
      auditActor(actor, req),
    )
  }

  @Post(':userId/reset-password')
  async resetPassword(
    @Param('slug') slug: string,
    @Param('userId') userId: string,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const { actor, tenant } = await this.gate(slug, jwt)
    return this.users.resetMemberPassword(
      { _id: tenant._id, slug: tenant.slug },
      userId,
      auditActor(actor, req),
    )
  }

  // Update a member's directory profile (display name + contact info) and/or
  // in-tenant role. Returns the fresh user doc with the tenant-scoped role,
  // mirroring GET :slug/users so the UI can swap the row in place.
  @Patch(':userId')
  async update(
    @Param('slug') slug: string,
    @Param('userId') userId: string,
    @Body() dto: UpdateTenantMemberDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const { actor, tenant } = await this.gate(slug, jwt)
    const updated = await this.users.updateTenantMember(
      { _id: tenant._id, slug: tenant.slug },
      userId,
      dto,
      auditActor(actor, req),
    )
    return { ...updated.toObject(), tenantRole: roleForTenant(updated, tenant._id) }
  }

  // Change a mailbox-less member's primary email (Authentik identity only).
  @Patch(':userId/primary-email')
  async changePrimaryEmail(
    @Param('slug') slug: string,
    @Param('userId') userId: string,
    @Body() dto: ChangePrimaryEmailDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const { actor, tenant } = await this.gate(slug, jwt)
    const updated = await this.users.changeMemberPrimaryEmail(
      { _id: tenant._id, slug: tenant.slug },
      userId,
      dto.email,
      auditActor(actor, req),
    )
    return { ...updated.toObject(), tenantRole: roleForTenant(updated, tenant._id) }
  }

  // Transfer workspace ownership to this member. Gated to tenant admins / the
  // current owner / platform admins — NOT only the owner, since the common case
  // is the owner having left. Demotes the previous owner to admin.
  @Post(':userId/make-owner')
  async makeOwner(
    @Param('slug') slug: string,
    @Param('userId') userId: string,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const { actor, tenant } = await this.gate(slug, jwt)
    const actorRole = roleForTenant(actor, tenant._id)
    if (!actor.platformAdmin && actorRole !== 'admin' && actorRole !== 'owner') {
      throw new ForbiddenException('Only an admin or the owner can transfer ownership.')
    }
    const res = await this.users.transferOwnership(
      { _id: tenant._id, slug: tenant.slug },
      userId,
      auditActor(actor, req),
    )
    return {
      newOwner: { ...res.newOwner.toObject(), tenantRole: roleForTenant(res.newOwner, tenant._id) },
      previousOwners: res.previousOwners.map((u) => ({
        ...u.toObject(),
        tenantRole: roleForTenant(u, tenant._id),
      })),
    }
  }

  // Provision a mailbox for a member who has none (e.g. the bootstrap admin who
  // signs in with an external email). Returns the new address + a one-time
  // password, plus the refreshed user doc so the UI can flip the panel.
  @Post(':userId/mailbox')
  async createMailbox(
    @Param('slug') slug: string,
    @Param('userId') userId: string,
    @Body() dto: CreateMailboxDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const { actor, tenant } = await this.gate(slug, jwt)
    const res = await this.users.createMailboxForMember(
      { _id: tenant._id, slug: tenant.slug },
      userId,
      dto,
      auditActor(actor, req),
    )
    return {
      email: res.email,
      tempPassword: res.tempPassword,
      user: { ...res.user.toObject(), tenantRole: roleForTenant(res.user, tenant._id) },
    }
  }

  // Mailbox aliases (extra addresses that deliver to the member's inbox).
  @Get(':userId/aliases')
  async listAliases(
    @Param('slug') slug: string,
    @Param('userId') userId: string,
    @CurrentUser() jwt: AuthentikJwtPayload,
  ) {
    const { tenant } = await this.gate(slug, jwt)
    return this.users.listMemberAliases({ _id: tenant._id, slug: tenant.slug }, userId)
  }

  @Post(':userId/aliases')
  async addAlias(
    @Param('slug') slug: string,
    @Param('userId') userId: string,
    @Body() dto: AddAliasDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const { actor, tenant } = await this.gate(slug, jwt)
    return this.users.addMemberAlias(
      { _id: tenant._id, slug: tenant.slug },
      userId,
      dto,
      auditActor(actor, req),
    )
  }

  @Delete(':userId/aliases')
  async removeAlias(
    @Param('slug') slug: string,
    @Param('userId') userId: string,
    @Body() dto: RemoveAliasDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const { actor, tenant } = await this.gate(slug, jwt)
    return this.users.removeMemberAlias(
      { _id: tenant._id, slug: tenant.slug },
      userId,
      dto.address,
      auditActor(actor, req),
    )
  }
}
