import {
  Body,
  Controller,
  Delete,
  ForbiddenException,
  Get,
  Param,
  Patch,
  Post,
  Put,
  Query,
  Req,
  UseGuards,
} from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import type { Model } from 'mongoose'
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 { PartnersService } from '../partners/partners.service.js'
import { Partner, PartnerDocument } from '../schemas/partner.schema.js'
import { CreateTenantDto } from '../tenants/dto/create-tenant.dto.js'
import { TenantsService } from '../tenants/tenants.service.js'
import { InvitePartnerUserDto } from '../users/dto/invite-partner-user.dto.js'
import { UsersService } from '../users/users.service.js'
import { CreateReportDto } from './dto/create-report.dto.js'
import { PartnerBrandingDto } from './dto/partner-branding.dto.js'
import { PartnerSettingsDto } from './dto/partner-settings.dto.js'
import { PartnerUpdateTenantDto } from './dto/partner-update-tenant.dto.js'
import { PartnerBrandingService } from './partner-branding.service.js'
import { PartnerReportsService } from './partner-reports.service.js'

// Self-service endpoints for the partner portal. Everything here scopes to
// the caller's resolved User.partnerId — no slug in any URL, no operator
// guard. A portal-aud JWT is sufficient; partner-staff is enforced per
// handler via the actor.partnerId check.
//
// Identity endpoints (GET /users/me, etc.) intentionally stay on
// UsersController — those are about "who am I", whereas everything here is
// about "what does my partner own".
@Controller('me/partner')
@UseGuards(JwtAuthGuard)
export class PartnerMeController {
  constructor(
    private readonly users: UsersService,
    private readonly tenants: TenantsService,
    private readonly partners: PartnersService,
    private readonly branding: PartnerBrandingService,
    private readonly reports: PartnerReportsService,
    private readonly actor: ActorService,
    @InjectModel(Partner.name) private readonly partnerModel: Model<PartnerDocument>,
  ) {}

  // The OTHER people at the caller's partner organization. Distinct from
  // GET /partners/:slug/users (operator only): scoped via the actor's
  // User.partnerId so a portal-aud JWT works.
  @Get('users')
  async listUsers(@CurrentUser() jwt: AuthentikJwtPayload) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    return this.users.listPartnerUsersEnriched(actor.partnerId)
  }

  // Self-service partner-staff invite. Counterpart to the operator-only
  // POST /partners/:slug/users — scoped to the caller's own partner.
  @Post('users')
  async inviteUser(
    @Body() dto: InvitePartnerUserDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    const partner = await this.partnerModel.findById(actor.partnerId, { slug: 1 }).exec()
    if (!partner) {
      throw new ForbiddenException('Partner record missing')
    }
    const auditActor = { userId: String(actor._id), email: actor.email, ip: clientIp(req) }
    return this.users.invitePartnerUser(dto, { _id: actor.partnerId, slug: partner.slug }, auditActor)
  }

  // Remove a teammate from the partner organization.
  @Delete('users/:subject')
  async removeUser(
    @Param('subject') subject: string,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    if (subject === jwt.sub) {
      throw new ForbiddenException('You cannot remove your own account')
    }
    const auditActor = { userId: String(actor._id), email: actor.email, ip: clientIp(req) }
    return this.users.removePartnerUser(subject, actor.partnerId, auditActor)
  }

  // Tenants (customers) attached to the partner.
  @Get('tenants')
  async listTenants(@CurrentUser() jwt: AuthentikJwtPayload) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    return this.users.listPartnerTenants(actor.partnerId)
  }

  // Self-service tenant create. Counterpart to operator POST /tenants.
  // Forces partnerId from actor.partnerId so a partner can never create
  // a tenant under a different partner, even if their payload says so.
  // If adminName + adminEmail are present, fires inviteTenantAdmin after
  // tenant provisioning. Admin failures don't roll back the tenant — the
  // response includes an `adminInvite` field with credentials or an error.
  @Post('tenants')
  async createTenant(
    @Body() dto: CreateTenantDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    const auditActor = {
      userId: String(actor._id),
      email: actor.email,
      ip: clientIp(req),
    }
    const safeDto = { ...dto, partnerId: actor.partnerId } as CreateTenantDto & {
      partnerId: typeof actor.partnerId
    }
    const tenant = await this.tenants.create(safeDto, auditActor)

    let adminInvite:
      | { subject: string; userId: string; attached?: boolean; link?: string; tempPassword?: string }
      | { error: string }
      | undefined
    if (dto.adminName && dto.adminEmail) {
      try {
        adminInvite = await this.users.inviteTenantAdmin(
          { _id: tenant._id, slug: tenant.slug, authentikGroupId: tenant.authentikGroupId },
          { name: dto.adminName, email: dto.adminEmail },
          auditActor,
        )
      } catch (err) {
        adminInvite = { error: err instanceof Error ? err.message : String(err) }
      }
    }

    return { tenant, adminInvite }
  }

  // Edit a customer the partner owns (name, industry, brand colour, domains,
  // seats). Ownership is enforced in TenantsService.partnerUpdate.
  @Patch('tenants/:slug')
  async updateTenant(
    @Param('slug') slug: string,
    @Body() dto: PartnerUpdateTenantDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    const auditActor = { userId: String(actor._id), email: actor.email, ip: clientIp(req) }
    return this.tenants.partnerUpdate(slug, actor.partnerId, dto, auditActor)
  }

  @Post('tenants/:slug/suspend')
  async suspendTenant(
    @Param('slug') slug: string,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    const auditActor = { userId: String(actor._id), email: actor.email, ip: clientIp(req) }
    return this.tenants.partnerSetStatus(slug, actor.partnerId, 'suspended', auditActor)
  }

  @Post('tenants/:slug/resume')
  async resumeTenant(
    @Param('slug') slug: string,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    const auditActor = { userId: String(actor._id), email: actor.email, ip: clientIp(req) }
    return this.tenants.partnerSetStatus(slug, actor.partnerId, 'active', auditActor)
  }

  // Monthly Recurring Revenue across the partner's customers — grouped by
  // currency since subs can be billed in DKK / EUR / USD independently.
  @Get('mrr')
  async mrr(@CurrentUser() jwt: AuthentikJwtPayload) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    return this.users.partnerMrr(actor.partnerId)
  }

  // Recent audit events across the partner's portfolio. Used by the
  // dashboard's Activity card and the /partner/audit page. Pagination via
  // ?before=<iso> + ?limit=N.
  @Get('activity')
  async activity(
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Query('limit') limit?: string,
    @Query('before') before?: string,
  ) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    const partner = await this.partnerModel.findById(actor.partnerId, { slug: 1 }).exec()
    if (!partner) {
      throw new ForbiddenException('Partner record missing')
    }
    return this.users.partnerActivity(actor.partnerId, partner.slug, {
      limit: limit ? Number(limit) : undefined,
      before: before ? new Date(before) : undefined,
    })
  }

  // Partner settings — profile, notification prefs, agreement (read-only),
  // documents (read-only), plus marginPct/contactInfo/billingInfo for display.
  @Get('settings')
  async getSettings(@CurrentUser() jwt: AuthentikJwtPayload) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    return this.partners.getSettings(actor.partnerId)
  }

  @Patch('settings')
  async updateSettings(
    @Body() dto: PartnerSettingsDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    const auditActor = { userId: String(actor._id), email: actor.email, ip: clientIp(req) }
    return this.partners.updateSettings(actor.partnerId, dto, auditActor)
  }

  // Whitelabel branding — identity, customer defaults, email templates.
  @Get('branding')
  async getBranding(@CurrentUser() jwt: AuthentikJwtPayload) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    return this.branding.get(actor.partnerId)
  }

  @Put('branding')
  async putBranding(
    @Body() dto: PartnerBrandingDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    const auditActor = { userId: String(actor._id), email: actor.email, ip: clientIp(req) }
    return this.branding.put(actor.partnerId, dto, auditActor)
  }

  // Live analytics for the reports page (health cohorts, revenue by plan, top
  // customers, signup/churn cohorts). Computed, not stored.
  @Get('reports')
  async getReports(@CurrentUser() jwt: AuthentikJwtPayload) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    return this.users.partnerReports(actor.partnerId)
  }

  // Saved/custom report definitions.
  @Get('reports/saved')
  async listSavedReports(@CurrentUser() jwt: AuthentikJwtPayload) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    return this.reports.list(actor.partnerId)
  }

  @Post('reports/saved')
  async createSavedReport(
    @Body() dto: CreateReportDto,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    const auditActor = { userId: String(actor._id), email: actor.email, ip: clientIp(req) }
    return this.reports.create(actor.partnerId, dto, auditActor)
  }

  @Delete('reports/saved/:id')
  async deleteSavedReport(
    @Param('id') id: string,
    @CurrentUser() jwt: AuthentikJwtPayload,
    @Req() req: Parameters<typeof clientIp>[0],
  ) {
    const actor = await this.actor.resolve(jwt)
    if (!actor.partnerId) {
      throw new ForbiddenException('Not a partner-staff user')
    }
    const auditActor = { userId: String(actor._id), email: actor.email, ip: clientIp(req) }
    return this.reports.remove(id, actor.partnerId, auditActor)
  }
}
