import {
  BadRequestException,
  Body,
  Controller,
  Get,
  Param,
  Post,
  Query,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { ZodResponse } from 'nestjs-zod';
import { Permission } from '@pac/types';
import {
  CurrentUser,
  AuthenticatedUser,
} from '../../common/decorators/current-user.decorator';
import { RequirePermission } from '../../common/decorators/permissions.decorator';
import {
  TenantScope,
  TenantScopeContext,
} from '../../common/decorators/tenant-scope.decorator';
import { resolveClinicId } from '../../common/decorators/resolve-clinic-id';
import {
  AssignPlanRequestDto,
  ListPlansQueryDto,
  ListPlansResponseDto,
  PlanCountsResponseDto,
  PlanActionAckDto,
  PlanDetailResponseDto,
  RecomputeAckDto,
  RecyclePlanRequestDto,
  SubmitExecutionRequestDto,
  SubmitExecutionResponseDto,
  ListExecutionsResponseDto,
} from './dto/plan.dto';
import { PlanService } from './plan.service';
import { PlanEngineService } from './engine/plan-engine.service';
import { CohortAttributesService } from './cohort-attributes.service';
import { resolveScriptAgent } from '../ai/calls/draft-plan-script/shared/agent-identity';

@ApiTags('plan')
@ApiBearerAuth('accessToken')
@Controller('plans')
export class PlanController {
  constructor(
    private readonly plans: PlanService,
    private readonly engine: PlanEngineService,
    private readonly cohorts: CohortAttributesService,
  ) {}

  @Get()
  @ZodResponse({ status: 200, type: ListPlansResponseDto })
  @RequirePermission(Permission.PLAN_VIEW_OWN)
  @ApiOperation({
    summary: 'List plans (recall pool, my queue, or by filters)',
    description:
      'Use ?view=pool to fetch the unassigned active pool, ?view=mine for my assigned plans, ' +
      '?view=all for everything (subject to permission). Without view, staff sees own only.',
  })
  list(
    @TenantScope() scope: TenantScopeContext,
    @CurrentUser() user: AuthenticatedUser,
    @Query() query: ListPlansQueryDto,
  ) {
    return this.plans.list(scope, query, user.permissions);
  }

  @Get('counts')
  @ZodResponse({ status: 200, type: PlanCountsResponseDto })
  @RequirePermission(Permission.PLAN_VIEW_OWN)
  @ApiOperation({
    summary: 'Plan counts for KPI / tab badges (one SQL roundtrip, replaces 5 list calls)',
  })
  counts(
    @TenantScope() scope: TenantScopeContext,
    @CurrentUser() user: AuthenticatedUser,
  ) {
    return this.plans.counts(scope, user.permissions);
  }

  /**
   * 「上次医生 / 偏好医生」筛选用的候选名单。
   *
   * ⚠️ 必须放在 `@Get(':id')` **之前** —— 否则 'doctors' 会被当成 planId 吃掉(Nest 按声明顺序匹配)。
   */
  @Get('doctors')
  @RequirePermission(Permission.PLAN_VIEW_OWN)
  @ApiOperation({
    summary: '本 scope 的医生名单(筛选面板的可搜索输入框用;结果缓存)',
    description:
      '取自 patients.last_visit_doctor / preferred_doctor 去重(事实派生列,非画像)。' +
      '前端在这个名单上做客户端模糊匹配,**选中后发精确姓名** —— 等值查询才吃得到偏索引。',
  })
  doctors(@TenantScope() scope: TenantScopeContext) {
    return this.plans.doctorOptions(scope);
  }

  /**
   * 初选矩阵:8 潜在治疗 × 3 温度 + 「待重算」列。
   *
   * ⚠️ 同 `doctors`,必须放在 `@Get(':id')` **之前**,否则 'matrix' 会被当成 planId。
   * ⚠️ 权限是 `PLAN_DISPATCH` 不是 `PLAN_VIEW_OWN` —— 矩阵是**召回池**的视图,
   *   而 T16 明写客服看不到池子。给成 VIEW_OWN 等于从后门把池子露给客服。
   */
  @Get('matrix')
  @RequirePermission(Permission.PLAN_DISPATCH)
  @ApiOperation({
    summary: '初选矩阵(潜在治疗 × 窗口温度),按患者去重',
    description:
      '口径是**去重患者数**,不是 plan 条数(列表页按 plan 分页)。' +
      '温度定义见 packages/types/temperature.ts:逐条 gap 用自己 K 码的窗口判档、取最热。',
  })
  matrix(@TenantScope() scope: TenantScopeContext, @Query('clinicId') clinicId?: string) {
    /**
     * 不传 clinicId 用登录人的第一个诊所 —— 批次不跨诊所,矩阵天然是诊所维度的。
     * 🔴 传了就**必须在范围里**:矩阵是那家诊所的**患者量分布**,
     *   2026-08-10 实测这里也是敞的(带别家 id 能拿到完整矩阵)。
     */
    return this.cohorts.matrix(scope, resolveClinicId(scope, clinicId));
  }

  @Get(':id')
  @ZodResponse({ status: 200, type: PlanDetailResponseDto })
  @RequirePermission(Permission.PLAN_VIEW_OWN)
  detail(
    @TenantScope() scope: TenantScopeContext,
    @CurrentUser() user: AuthenticatedUser,
    @Param('id') id: string,
  ) {
    // 话术里的【回访客服】按当前登录人回填(见 agent-identity.ts)
    return this.plans.detail(scope, id, resolveScriptAgent(user));
  }

  @Post(':id/assign')
  @ZodResponse({ status: 200, type: PlanActionAckDto })
  @RequirePermission(Permission.PLAN_ASSIGN)
  async assign(
    @TenantScope() scope: TenantScopeContext,
    @CurrentUser() user: AuthenticatedUser,
    @Param('id') id: string,
    @Body() dto: AssignPlanRequestDto,
  ) {
    // 传操作人 —— 账本据此区分自认领(actor==assignee)与指派他人
    await this.plans.assign(scope, id, dto.assigneeUserId, user.sub);
    return { ok: true as const };
  }

  @Post(':id/recycle')
  @ZodResponse({ status: 200, type: PlanActionAckDto })
  @RequirePermission(Permission.PLAN_RECYCLE)
  async recycle(
    @TenantScope() scope: TenantScopeContext,
    @CurrentUser() user: AuthenticatedUser,
    @Param('id') id: string,
    @Body() dto: RecyclePlanRequestDto,
  ) {
    // 能不能返**别人**的单,看有没有 PLAN_VIEW_ALL(leader/admin 有,staff 没有)——
    // staff 现在也能返池,但只能退自己认领的,校验在 service 里(见 recycle 注释)。
    const canRecycleOthers = user.permissions.includes(Permission.PLAN_VIEW_ALL);
    // ⚠️ 这里原本写的是 `@Body() _dto` —— 下划线,收了就扔。退回原因在链路第一步蒸发,
    //   「客服为什么不接这单」永远统计不出来。透传下去别再丢。
    await this.plans.recycle(
      scope,
      id,
      user.sub,
      canRecycleOthers,
      dto.releaseReason,
      dto.releaseNote,
    );
    return { ok: true as const };
  }

  @Post(':id/executions')
  @ZodResponse({ status: 201, type: SubmitExecutionResponseDto })
  @RequirePermission(Permission.PLAN_EXECUTE)
  @ApiOperation({
    summary:
      '接口 2: write a Plan execution outcome. Strict invariant — execution feedback ' +
      'NEVER promotes to PatientFactEvent.',
  })
  submitExecution(
    @TenantScope() scope: TenantScopeContext,
    @CurrentUser() user: AuthenticatedUser,
    @Param('id') id: string,
    @Body() dto: SubmitExecutionRequestDto,
  ) {
    return this.plans.submitExecution(scope, id, user.sub, dto);
  }

  @Get(':id/executions')
  @ZodResponse({ status: 200, type: ListExecutionsResponseDto })
  @RequirePermission(Permission.PLAN_VIEW_OWN)
  @ApiOperation({ summary: '某 plan 的执行历史(给"通话记录"面板)' })
  listExecutions(
    @TenantScope() scope: TenantScopeContext,
    @Param('id') id: string,
  ) {
    return this.plans.listExecutions(scope, id);
  }

  @Post(':id/recompute')
  @ZodResponse({ status: 202, type: RecomputeAckDto })
  @RequirePermission(Permission.PLAN_ASSIGN)
  @ApiOperation({
    summary: 'Force a plan recompute for the patient owning this plan (ops/dev tool)',
  })
  async recompute(@TenantScope() scope: TenantScopeContext, @Param('id') id: string) {
    const plan = await this.plans.detail(scope, id);
    const result = await this.engine.recomputeForPatient({
      hostId: scope.hostId,
      tenantId: scope.tenantId,
      // 新 schema:Plan 没 clinicId 字段;target_clinic_id 可空(集团统一池场景)
      clinicId: plan.plan.targetClinicId ?? '',
      patientId: plan.plan.patientId,
    });
    return { accepted: true as const, plansCreated: result.plansCreated };
  }

  @Post('recompute/by-patient/:patientId')
  @ZodResponse({ status: 202, type: RecomputeAckDto })
  @RequirePermission(Permission.PLAN_ASSIGN)
  @ApiOperation({
    summary: 'Recompute plans for a patient that has no plan yet (ops/dev cold-start tool)',
  })
  async recomputeByPatient(
    @TenantScope() scope: TenantScopeContext,
    @Param('patientId') patientId: string,
  ) {
    const result = await this.plans.recomputeByPatient(scope, patientId, this.engine);
    return { accepted: true as const, plansCreated: result.plansCreated };
  }
}
