import {
  Controller,
  Get,
  Post,
  Put,
  Patch,
  Delete,
  Param,
  Body,
  Query,
  Req,
  Res,
  Logger,
} from '@nestjs/common';
import type { Request, Response } from 'express';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { DingtalkAuthService } from './sdk/dingtalk-auth.service';
import { DingtalkHrmService } from './sdk/dingtalk-hrm.service';
import { DingtalkAttendanceService } from './sdk/dingtalk-attendance.service';
import { DingtalkYidaService } from './sdk/dingtalk-yida.service';
import { DingtalkSchedulerService } from './dingtalk-scheduler.service';
import {
  AnnualLeavePlanSettingsQueryDto,
  ApprovalCancellationDto,
  ExecutionQueryDto,
  ManualTriggerDto,
  PaginationQueryDto,
  UpdateAnnualLeaveQuotaDto,
  UpdateAnnualLeavePlanSettingsDto,
} from './dto/dingtalk-sync.dto';
import { SyncLogger } from './sync/sync-logger';
import {
  FORM_UUIDS, APPROVED_SEARCH_CONDITION, BUSINESS_TRIP_FIELDS,
  BUSINESS_TRIP_CHANGE_FIELDS, BUSINESS_TRIP_CHANGE_TYPES, FIELD_APPLICATION_FIELDS,
  getAllLeaveCodesFromCache, getLeaveTypeName,
} from './constants';
import { getDingtalkSerialNumber } from './utils';
import { DingtalkRepairService } from './dingtalk-repair.service';
import { AnnualLeaveInsightService } from './annual-leave-insight.service';
import { AnnualLeavePlanAdminService } from './annual-leave-plan-admin.service';
import { EmployeeManagementService } from './employee-management.service';
import { PurchaseSapSyncService } from './sync/purchase-sap-sync.service';
import { SapEnvironment } from './sap/sap-purchase.service';

// 任务元数据
const TASK_METADATA: Record<string, { name: string; description: string; group: string; schedule: string }> = {
  DINGTALK_BUSINESS_TRIP: { name: '出差同步', description: '将宜搭出差申请及变更同步到钉钉考勤', group: '考勤同步', schedule: '每小时 :15/:45' },
  DINGTALK_FIELD_APPLICATION: { name: '外勤同步', description: '将宜搭外勤申请同步到钉钉考勤', group: '考勤同步', schedule: '每小时 :15/:45' },
  DINGTALK_OVERTIME: { name: '加班同步', description: '将宜搭加班确认同步到钉钉考勤（加班转调休）', group: '考勤同步', schedule: '每小时 :15/:45' },
  DINGTALK_EMPLOYEE_INFO: { name: '员工信息同步', description: '钉钉HR员工信息同步到宜搭表单（每周更新）', group: '员工与假期', schedule: '每天 01:11' },
  DINGTALK_EMPLOYEE_INFO_OLD: { name: '员工信息同步(旧表)', description: '钉钉HR员工信息同步到宜搭旧表', group: '员工与假期', schedule: '每天 02:11' },
  DINGTALK_ANNUAL_LEAVE: { name: '年假释放', description: '根据工龄/司龄计算并释放年假到钉钉', group: '员工与假期', schedule: '每天 03:11' },
  DINGTALK_LEAVE_EXTENSION: { name: '假期延期', description: '处理宜搭假期延期申请并更新钉钉假期额度', group: '员工与假期', schedule: '每小时 :15/:45' },
  DINGTALK_LEAVE_REMINDER: { name: '假期到期提醒', description: '每周五通知即将到期的假期延期', group: '员工与假期', schedule: '每周五 00:01' },
  DINGTALK_PURCHASE_SAP_PROD: { name: '采购申请SAP同步(生产)', description: '将宜搭采购申请同步到SAP生产环境', group: 'SAP集成', schedule: '每小时 :20/:50' },
  DINGTALK_PURCHASE_SAP_TEST: { name: '采购申请SAP同步(测试)', description: '将宜搭采购申请同步到SAP测试环境', group: 'SAP集成', schedule: '仅手动触发' },
};

@Controller('organization/dingtalk')
export class DingtalkController {
  private readonly logger = new Logger(DingtalkController.name);
  private employeeCache: { data: { userId: string; name: string }[]; expireAt: number } = { data: [], expireAt: 0 };

  constructor(
    private prisma: PrismaService,
    private authService: DingtalkAuthService,
    private hrmService: DingtalkHrmService,
    private attendanceService: DingtalkAttendanceService,
    private yidaService: DingtalkYidaService,
    private schedulerService: DingtalkSchedulerService,
    private repairService: DingtalkRepairService,
    private annualLeaveInsightService: AnnualLeaveInsightService,
    private annualLeavePlanAdminService: AnnualLeavePlanAdminService,
    private employeeManagementService: EmployeeManagementService,
    private purchaseSapSyncService: PurchaseSapSyncService,
  ) {}

  /**
   * GET /api/v1/organization/dingtalk/overview
   * 获取钉钉同步总览
   */
  @Get('overview')
  async getOverview() {
    const tasks = await this.prisma.automationTask.findMany({
      where: { type: 'DINGTALK_SYNC' },
      orderBy: { code: 'asc' },
    });

    const today = new Date();
    today.setHours(0, 0, 0, 0);

    const todayExecutions = await this.prisma.automationExecution.findMany({
      where: {
        task: { type: 'DINGTALK_SYNC' },
        startedAt: { gte: today },
      },
    });

    const running = todayExecutions.filter(e => e.status === 'RUNNING').length;
    const todaySuccess = todayExecutions.filter(e => e.status === 'SUCCESS').length;
    const todayFailed = todayExecutions.filter(e => e.status === 'FAILED').length;

    return {
      totalTasks: Object.keys(TASK_METADATA).length,
      registeredTasks: tasks.length,
      running,
      todaySuccess,
      todayFailed,
      isEnabled: this.authService.isEnabled,
    };
  }

  /**
   * GET /api/v1/organization/dingtalk/tasks
   * 获取所有钉钉同步任务列表
   */
  @Get('tasks')
  async getTasks() {
    const tasks = await this.prisma.automationTask.findMany({
      where: { type: 'DINGTALK_SYNC' },
      orderBy: { code: 'asc' },
    });

    const taskMap = new Map(tasks.map(t => [t.code, t]));

    return Object.entries(TASK_METADATA).map(([code, meta]) => {
      const task = taskMap.get(code);
      return {
        code,
        ...meta,
        status: task?.status || 'ACTIVE',
        lastRunAt: task?.lastRunAt || null,
        lastStatus: task?.lastStatus || null,
        totalRuns: task?.totalRuns || 0,
        successRuns: task?.successRuns || 0,
        failedRuns: task?.failedRuns || 0,
      };
    });
  }

  /**
   * POST /api/v1/organization/dingtalk/tasks/:code/trigger
   * 手动触发指定同步任务
   */
  @Post('tasks/:code/trigger')
  async triggerTask(
    @Param('code') code: string,
    @Body() dto: ManualTriggerDto,
  ) {
    if (!TASK_METADATA[code]) {
      return { success: false, error: `未知任务代码: ${code}` };
    }

    const result = await this.schedulerService.triggerTask(
      code,
      dto.fromTime,
      dto.toTime,
      undefined,
      dto.userId,
    );

    return {
      ...result,
      taskCode: code,
      taskName: TASK_METADATA[code].name,
    };
  }

  /**
   * GET /api/v1/organization/dingtalk/tasks/:code/trigger-stream
   * SSE 实时日志流式触发同步任务
   */
  @Get('tasks/:code/trigger-stream')
  async triggerTaskStream(
    @Param('code') code: string,
    @Req() req: Request,
    @Res() res: Response,
    @Query('fromTime') fromTime?: string,
    @Query('toTime') toTime?: string,
    @Query('userId') userId?: string,
  ) {
    if (!TASK_METADATA[code]) {
      res.status(400).json({ success: false, error: `未知任务代码: ${code}` });
      return;
    }

    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    res.setHeader('X-Accel-Buffering', 'no');

    let clientDisconnected = false;
    req.on('close', () => {
      clientDisconnected = true;
    });

    const safeSend = (data: string) => {
      if (!clientDisconnected) {
        try {
          res.write(data);
        } catch {
          clientDisconnected = true;
        }
      }
    };

    const logger = new SyncLogger('TriggerStream', (entry) => {
      safeSend(`data: ${JSON.stringify({ type: 'log', ...entry })}\n\n`);
    });

    try {
      const result = await this.schedulerService.triggerTask(
        code,
        fromTime || undefined,
        toTime || undefined,
        undefined,
        userId || undefined,
        logger,
      );
      safeSend(`data: ${JSON.stringify({ type: 'done', ...result })}\n\n`);
    } catch (error: any) {
      safeSend(`data: ${JSON.stringify({ type: 'error', message: error.message })}\n\n`);
    }

    if (!clientDisconnected) {
      res.end();
    }
  }

  /**
   * GET /api/v1/organization/dingtalk/tasks/:code/executions
   * 获取指定任务的执行历史
   */
  @Get('tasks/:code/executions')
  async getTaskExecutions(
    @Param('code') code: string,
    @Query() query: PaginationQueryDto,
  ) {
    const page = parseInt(query.page || '1');
    const limit = parseInt(query.limit || '20');

    const task = await this.prisma.automationTask.findFirst({
      where: { code },
    });

    if (!task) {
      return { items: [], total: 0, page, limit };
    }

    const [items, total] = await Promise.all([
      this.prisma.automationExecution.findMany({
        where: { taskId: task.id },
        orderBy: { startedAt: 'desc' },
        skip: (page - 1) * limit,
        take: limit,
      }),
      this.prisma.automationExecution.count({
        where: { taskId: task.id },
      }),
    ]);

    return { items, total, page, limit };
  }

  /**
   * GET /api/v1/organization/dingtalk/executions/recent
   * 获取最近的执行记录（支持任务类型筛选、日期范围、分页）
   */
  @Get('executions/recent')
  async getRecentExecutions(@Query() query: ExecutionQueryDto) {
    const page = parseInt(query.page || '1');
    const limit = parseInt(query.limit || '20');

    const taskFilter: any = { type: 'DINGTALK_SYNC' };
    if (query.taskCode) {
      taskFilter.code = query.taskCode;
    }

    const where: any = {
      task: { is: taskFilter },
    };

    if (query.startDate || query.endDate) {
      where.startedAt = {};
      if (query.startDate) where.startedAt.gte = new Date(query.startDate);
      if (query.endDate) {
        const end = new Date(query.endDate);
        end.setHours(23, 59, 59, 999);
        where.startedAt.lte = end;
      }
    }

    // 隐藏处理数量为 0 的记录（result JSON 中 processedCount > 0）
    if (query.hideEmpty === 'true') {
      where.result = {
        path: ['processedCount'],
        gt: 0,
      };
    }

    const [executions, total] = await Promise.all([
      this.prisma.automationExecution.findMany({
        where,
        include: {
          task: { select: { code: true, name: true } },
        },
        orderBy: { startedAt: 'desc' },
        skip: (page - 1) * limit,
        take: limit,
      }),
      this.prisma.automationExecution.count({ where }),
    ]);

    return {
      items: executions.map(e => ({
        id: e.id,
        taskCode: e.task.code,
        taskName: TASK_METADATA[e.task.code]?.name || e.task.name,
        status: e.status,
        startedAt: e.startedAt,
        completedAt: e.completedAt,
        duration: e.duration,
        triggerType: e.triggerType,
        error: e.error,
        result: e.result,
        logs: e.logs,
      })),
      total,
      page,
      limit,
    };
  }

  /**
   * GET /api/v1/organization/dingtalk/employees/list
   * 获取本地员工列表（分页、搜索、筛选）
   */
  @Get('employees/list')
  async getEmployeeList(
    @Query('keyword') keyword?: string,
    @Query('status') status?: string,
    @Query('page') page?: string,
    @Query('pageSize') pageSize?: string,
  ) {
    return this.employeeManagementService.getEmployees({
      keyword: keyword || undefined,
      status: status || undefined,
      page: page ? parseInt(page) : 1,
      pageSize: pageSize ? parseInt(pageSize) : 50,
    });
  }

  /**
   * PUT /api/v1/organization/dingtalk/employees/:userId
   * 更新员工信息
   */
  @Put('employees/:userId')
  async updateEmployee(
    @Param('userId') userId: string,
    @Body() body: { status?: string },
  ) {
    return this.employeeManagementService.updateEmployee(userId, body);
  }

  // ========== 员工在职周期（支持离职再入职） ==========

  @Get('employees/:userId/employment-periods')
  async getEmploymentPeriods(@Param('userId') userId: string) {
    return this.employeeManagementService.getEmploymentPeriods(userId);
  }

  @Post('employees/:userId/employment-periods')
  async addEmploymentPeriod(
    @Param('userId') userId: string,
    @Body() body: { joinDate: string; leaveDate?: string | null; countInTenure?: boolean; note?: string },
  ) {
    return this.employeeManagementService.addEmploymentPeriod(userId, body);
  }

  @Put('employees/:userId/employment-periods/:id')
  async updateEmploymentPeriod(
    @Param('id') id: string,
    @Body() body: { joinDate?: string; leaveDate?: string | null; countInTenure?: boolean; note?: string },
  ) {
    return this.employeeManagementService.updateEmploymentPeriod(id, body);
  }

  @Delete('employees/:userId/employment-periods/:id')
  async deleteEmploymentPeriod(@Param('id') id: string) {
    return this.employeeManagementService.deleteEmploymentPeriod(id);
  }

  @Get('employees/:userId/tenure')
  async getTenure(@Param('userId') userId: string) {
    return this.employeeManagementService.calculateTenureDays(userId);
  }

  @Post('employees/employment-periods/init')
  async initEmploymentPeriods() {
    return this.employeeManagementService.initEmploymentPeriods();
  }

  @Post('employees/tenure/recalculate')
  async recalculateTenure() {
    return this.employeeManagementService.recalculateAllTenure();
  }

  // ========== 停薪留职 ==========

  @Get('employees/:userId/suspension-periods')
  async getSuspensionPeriods(@Param('userId') userId: string) {
    return this.employeeManagementService.getSuspensionPeriods(userId);
  }

  @Post('employees/:userId/suspension-periods')
  async addSuspensionPeriod(
    @Param('userId') userId: string,
    @Body() body: { startDate: string; endDate?: string | null; reason?: string; note?: string },
  ) {
    return this.employeeManagementService.addSuspensionPeriod(userId, body);
  }

  @Put('employees/:userId/suspension-periods/:id')
  async updateSuspensionPeriod(
    @Param('id') id: string,
    @Body() body: { startDate?: string; endDate?: string | null; reason?: string; note?: string },
  ) {
    return this.employeeManagementService.updateSuspensionPeriod(id, body);
  }

  @Delete('employees/:userId/suspension-periods/:id')
  async deleteSuspensionPeriod(@Param('id') id: string) {
    return this.employeeManagementService.deleteSuspensionPeriod(id);
  }

  /**
   * POST /api/v1/organization/dingtalk/employees/sync
   * 手动触发员工同步（含部门刷新）
   */
  @Post('employees/sync')
  async syncEmployees() {
    return this.employeeManagementService.syncEmployeesFromDingtalk({ syncDepartments: true });
  }

  /**
   * GET /api/v1/organization/dingtalk/employees
   * 获取在职员工列表（姓名 + userId），30 分钟内存缓存
   */
  @Get('employees')
  async getEmployees() {
    if (Date.now() < this.employeeCache.expireAt && this.employeeCache.data.length > 0) {
      return this.employeeCache.data;
    }

    const ids = await this.hrmService.getOnJobEmployeeIds();
    const infoMap = await this.hrmService.getEmployeeInfoByIds(ids);

    const list = ids.map(id => {
      const employee = infoMap[id];
      if (!employee) return { userId: id, name: id };
      const fieldDataList = employee.field_data_list || [];
      const nameField = fieldDataList.find((f: any) => f.field_name === '姓名');
      const name = nameField?.field_value_list?.[0]?.value || id;
      return { userId: id, name };
    });

    this.employeeCache = { data: list, expireAt: Date.now() + 30 * 60_000 };
    return list;
  }

  /**
   * GET /api/v1/organization/dingtalk/config
   * 获取钉钉连接状态
   */
  @Get('config')
  async getConfig() {
    const enabled = await this.authService.getIsEnabled();

    let tokenValid = false;
    try {
      await this.authService.getAccessToken();
      tokenValid = true;
    } catch {
      tokenValid = false;
    }

    return {
      enabled,
      tokenValid,
      appType: this.authService.appType ? '已配置' : '未配置',
      operatorId: this.authService.operatorId ? '已配置' : '未配置',
    };
  }

  /**
   * GET /api/v1/organization/dingtalk/annual-leave/quotas
   * 获取钉钉假期余额总览
   */
  @Get('annual-leave/quotas')
  async getAnnualLeaveQuotas(
    @Query('userId') userId?: string,
    @Query('keyword') keyword?: string,
    @Query('hideZero') hideZero?: string,
    @Query('includeAllStatuses') includeAllStatuses?: string,
  ) {
    return this.annualLeaveInsightService.getQuotaOverview({
      userId: userId || undefined,
      keyword: keyword || undefined,
      hideZero: hideZero === 'true',
      includeAllStatuses: includeAllStatuses === 'true',
    });
  }

  /**
   * GET /api/v1/organization/dingtalk/annual-leave/details
   * 获取指定员工某个假期类型的余额详情、释放记录和使用记录
   */
  @Get('annual-leave/details')
  async getAnnualLeaveQuotaDetail(
    @Query('userId') userId?: string,
    @Query('leaveCode') leaveCode?: string,
  ) {
    if (!userId || !leaveCode) {
      return null;
    }

    return this.annualLeaveInsightService.getQuotaDetail({ userId, leaveCode });
  }

  /**
   * GET /api/v1/organization/dingtalk/annual-leave/records
   * 获取指定员工的假期消费记录（调用钉钉实时 API）
   */
  @Get('annual-leave/records')
  async getLeaveRecords(
    @Query('userId') userId: string,
    @Query('leaveCode') leaveCode: string,
  ) {
    if (!userId || !leaveCode) {
      return { records: [], error: '缺少 userId 或 leaveCode 参数' };
    }
    const records = await this.attendanceService.getLeaveRecords(leaveCode, userId);
    // 返回第一条原始记录用于前端调试字段结构
    const sampleKeys = records.length > 0 ? Object.keys(records[0]) : [];
    const sample = records.length > 0 ? records[0] : null;
    return { records, sampleKeys, sample };
  }

  /**
   * POST /api/v1/organization/dingtalk/annual-leave/leave-types/refresh
   * 从钉钉拉取企业所有假期类型并缓存
   */
  @Post('annual-leave/leave-types/refresh')
  async refreshLeaveTypes() {
    return this.annualLeaveInsightService.refreshLeaveTypes();
  }

  /**
   * POST /api/v1/organization/dingtalk/annual-leave/quotas/refresh
   * 手动刷新：先同步员工+假期类型，再刷新配额快照
   */
  @Post('annual-leave/quotas/refresh')
  async refreshAnnualLeaveQuotas(@Body() body: { userId?: string }) {
    // 手动刷新时全量更新数据源
    if (!body.userId) {
      await Promise.all([
        this.employeeManagementService.syncEmployeesFromDingtalk({ syncDepartments: true }),
        this.annualLeaveInsightService.refreshLeaveTypes(),
      ]);
    }
    return this.annualLeaveInsightService.refreshQuotaSnapshot({
      userId: body.userId || undefined,
    });
  }

  /**
   * POST /api/v1/organization/dingtalk/annual-leave/release-plan/refresh
   * 手动重算本地年假释放计划，不直接发放钉钉额度
   */
  @Post('annual-leave/release-plan/refresh')
  async refreshAnnualLeaveReleasePlan(@Body() body: { userId?: string; year?: number }) {
    return this.schedulerService.refreshAnnualLeavePlan(body.userId || undefined, body.year);
  }

  /**
   * POST /api/v1/organization/dingtalk/annual-leave/release
   * 手动触发年假释放（读中间表，发放到钉钉）
   */
  @Post('annual-leave/release')
  async triggerAnnualLeaveRelease(@Body() body: { userId?: string; year?: number }) {
    return this.schedulerService.triggerAnnualLeaveRelease(body.userId, body.year);
  }

  /**
   * PATCH /api/v1/organization/dingtalk/annual-leave/quota
   * 手动修改指定员工某年的年假额度（直接调钉钉 API）
   */
  @Patch('annual-leave/quota')
  async updateAnnualLeaveQuota(@Body() body: UpdateAnnualLeaveQuotaDto) {
    const { userId, year, totalDays } = body;

    // 查找该年的年假 leave_code
    const allCodes = getAllLeaveCodesFromCache();
    let leaveCode: string | null = null;
    for (const code of allCodes) {
      const name = getLeaveTypeName(code);
      if (name && name.includes(`${year}`) && name.includes('年假')) {
        leaveCode = code;
        break;
      }
    }
    if (!leaveCode) {
      return { success: false, error: `找不到 ${year} 年年假对应的假期类型` };
    }

    // 查询当前额度
    const currentQuotas = await this.attendanceService.searchLeaveQuota(leaveCode, [userId]);
    const currentQuota = currentQuotas.find((q: any) => q.quota_cycle === String(year));
    const oldDays = currentQuota ? Number(currentQuota.quota_num_per_day || 0) / 100 : 0;

    // 构造发放参数
    const quotaNum = Math.floor(totalDays * 100);
    const startTimestamp = new Date(year, 0, 1).getTime();
    const endTimestamp = new Date(year + 1, 2, 31, 23, 59, 59).getTime();

    const result = await this.attendanceService.updateLeaveQuota([{
      userid: userId,
      start_time: startTimestamp,
      end_time: endTimestamp,
      reason: '管理员手动调整年假额度',
      quota_num_per_day: quotaNum,
      quota_cycle: String(year),
      leave_code: leaveCode,
    }]);

    if (result.errcode !== 0) {
      return { success: false, error: `钉钉 API 错误: ${result.errmsg}` };
    }

    // 查员工姓名
    const emp = await (this.prisma as any).dingtalkEmployee.findFirst({
      where: { userId },
      select: { name: true },
    });

    return {
      success: true,
      detail: {
        name: emp?.name || userId,
        userId,
        year,
        oldDays,
        newDays: totalDays,
      },
    };
  }

  /**
   * GET /api/v1/organization/dingtalk/annual-leave/plan-settings
   * 获取员工年假计划参数
   */
  @Get('annual-leave/plan-settings')
  async getAnnualLeavePlanSettings(@Query() query: AnnualLeavePlanSettingsQueryDto) {
    return this.annualLeavePlanAdminService.getPlanSettings(query.userId, query.year);
  }

  /**
   * PATCH /api/v1/organization/dingtalk/annual-leave/plan-settings
   * 更新员工年假计划参数并重算本地释放计划
   */
  @Patch('annual-leave/plan-settings')
  async updateAnnualLeavePlanSettings(@Body() body: UpdateAnnualLeavePlanSettingsDto) {
    return this.annualLeavePlanAdminService.updatePlanSettings({
      userId: body.userId,
      year: body.year,
      adjustmentDays: body.adjustmentDays,
      notCountDays: body.notCountDays,
      recalculate: body.recalculate,
    });
  }

  /**
   * GET /api/v1/organization/dingtalk/annual-leave/release-plan
   * 获取年假释放计划
   */
  @Get('annual-leave/release-plan')
  async getAnnualLeaveReleasePlan(
    @Query('year') year?: string,
    @Query('userId') userId?: string,
    @Query('keyword') keyword?: string,
    @Query('upcomingOnly') upcomingOnly?: string,
  ) {
    return this.annualLeaveInsightService.getReleasePlan({
      year: year ? Number(year) : undefined,
      userId: userId || undefined,
      keyword: keyword || undefined,
      upcomingOnly: upcomingOnly === 'true',
    });
  }

  /**
   * GET /api/v1/organization/dingtalk/annual-leave/release-plan/export
   * 导出年假释放计划为 CSV
   */
  @Get('annual-leave/release-plan/export')
  async exportAnnualLeaveReleasePlan(
    @Res() res: Response,
    @Query('year') year?: string,
    @Query('userId') userId?: string,
    @Query('keyword') keyword?: string,
    @Query('upcomingOnly') upcomingOnly?: string,
  ) {
    const data = await this.annualLeaveInsightService.getReleasePlan({
      year: year ? Number(year) : undefined,
      userId: userId || undefined,
      keyword: keyword || undefined,
      upcomingOnly: upcomingOnly === 'true',
    });

    // 按员工+年度聚合为矩阵行
    const grouped = new Map<string, { name: string; userId: string; employeeNumber: string; status: string; year: string; totalDays: number; dates: string[]; hasPlan: boolean; noPlanReason?: string | null }>();
    for (const item of data.items) {
      const key = `${item.userId}-${item.year}`;
      const row = grouped.get(key) ?? { name: item.name, userId: item.userId, employeeNumber: item.employeeNumber, status: item.status, year: item.year, totalDays: 0, dates: [], hasPlan: item.hasPlan, noPlanReason: item.noPlanReason };
      if (item.hasPlan) {
        row.totalDays += Math.max(item.days, 0);
        for (let i = 0; i < Math.max(item.days, 0); i++) row.dates.push(item.date);
      }
      if (!item.hasPlan && item.noPlanReason) row.noPlanReason = item.noPlanReason;
      grouped.set(key, row);
    }

    const rows = [...grouped.values()].sort((a, b) => a.name.localeCompare(b.name, 'zh-CN') || a.userId.localeCompare(b.userId));
    const maxDays = rows.reduce((max, r) => Math.max(max, r.dates.length), 0);

    // 构造 CSV
    const csvEscape = (v: string) => v.includes(',') || v.includes('"') || v.includes('\n') ? `"${v.replace(/"/g, '""')}"` : v;
    const headers = ['姓名', '工号', '钉钉UserID', '状态', '总计划天数', ...Array.from({ length: maxDays }, (_, i) => `第${i + 1}天`)];
    const csvLines = [headers.join(',')];
    for (const row of rows) {
      const cols = [row.name, row.employeeNumber, row.userId, row.status, row.hasPlan ? String(row.totalDays) : '未生成'];
      for (let i = 0; i < maxDays; i++) cols.push(row.dates[i] || '');
      csvLines.push(cols.map(csvEscape).join(','));
    }

    const bom = '\uFEFF';
    const csv = bom + csvLines.join('\n');
    const targetYear = year || String(new Date().getFullYear());

    res.setHeader('Content-Type', 'text/csv; charset=utf-8');
    res.setHeader('Content-Disposition', `attachment; filename="annual-leave-release-plan-${targetYear}.csv"`);
    res.send(csv);
  }

  /**
   * PATCH /api/v1/organization/dingtalk/config
   * 更新全局配置（启用/禁用同步）
   */
  @Patch('config')
  async updateConfig(@Body() body: { enabled?: boolean }) {
    if (typeof body.enabled === 'boolean') {
      await this.authService.setEnabled(body.enabled);
    }
    return { success: true, enabled: await this.authService.getIsEnabled() };
  }

  /**
   * PATCH /api/v1/organization/dingtalk/tasks/:code
   * 启用/禁用任务
   */
  @Patch('tasks/:code')
  async updateTask(
    @Param('code') code: string,
    @Body() body: { status?: string },
  ) {
    const meta = TASK_METADATA[code];
    if (!meta) {
      return { success: false, error: '未知的任务代码' };
    }

    const task = await this.prisma.automationTask.upsert({
      where: { code },
      update: { status: body.status as any },
      create: {
        code,
        name: meta.name,
        type: 'DINGTALK_SYNC',
        scheduleType: 'CRON',
        status: body.status as any || 'ACTIVE',
      },
    });

    return { success: true, task };
  }

  /**
   * POST /api/v1/organization/dingtalk/batch-cancel
   * 批量取消考勤审批记录（临时修复端点）
   */
  @Post('batch-cancel')
  async batchCancel(
    @Body() body: { records: { userId: string; approveId: string }[]; dryRun?: boolean },
  ) {
    const results: any[] = [];
    for (const record of body.records) {
      if (body.dryRun) {
        results.push({ ...record, status: 'DRY_RUN' });
        continue;
      }
      try {
        const resp = await this.attendanceService.approveCancel(record.userId, record.approveId);
        results.push({ ...record, status: resp.errcode === 0 ? 'CANCELLED' : 'FAILED', response: resp });
        this.logger.log(`取消考勤: ${record.userId} / ${record.approveId} => ${resp.errcode}`);
      } catch (error: any) {
        results.push({ ...record, status: 'ERROR', error: error.message });
      }
    }
    return { total: body.records.length, results };
  }

  /**
   * POST /api/v1/organization/dingtalk/approval-cancellations
   * 人工修复：按同步类型、员工、审批号扫描并撤销考勤审批
   */
  @Post('approval-cancellations')
  async cancelApprovals(@Body() body: ApprovalCancellationDto) {
    return this.repairService.cancelApprovals(body);
  }

  /**
   * POST /api/v1/organization/dingtalk/debug/cleanup-invalid-field-approvals
   * 清理错误写入的外勤考勤记录（钉钉考勤侧 procInst_id/approve_id 形如 undefined-*）
   */
  @Post('debug/cleanup-invalid-field-approvals')
  async cleanupInvalidFieldApprovals(
    @Body() body: { from?: string; to?: string; userId?: string; dryRun?: boolean },
  ) {
    const data = await this.yidaService.searchApprovedInstances(
      FORM_UUIDS.FIELD_APPLICATION,
      body.from,
      body.to,
      body.userId,
    );

    const workDates = new Map<string, { userId: string; date: string; creator: string; instanceIds: string[] }>();
    for (const item of data) {
      const formData = item.formData || {};
      const creator = formData[FIELD_APPLICATION_FIELDS.CREATOR] || item.creatorUserId || 'unknown';
      const fieldDatas = formData[FIELD_APPLICATION_FIELDS.TABLE_FIELD] || [];
      const creatorUserId = item.creatorUserId;
      const instanceId = item.formInstanceId;

      for (const fieldData of fieldDatas) {
        const fromTs = fieldData?.[FIELD_APPLICATION_FIELDS.FROM_DATE];
        const toTs = fieldData?.[FIELD_APPLICATION_FIELDS.TO_DATE];
        if (!fromTs || !toTs) continue;

        for (const date of this.listCoveredDates(fromTs, toTs)) {
          const key = `${creatorUserId}|${date}`;
          const existing = workDates.get(key);
          if (existing) {
            if (!existing.instanceIds.includes(instanceId)) existing.instanceIds.push(instanceId);
            continue;
          }
          workDates.set(key, {
            userId: creatorUserId,
            date,
            creator,
            instanceIds: [instanceId],
          });
        }
      }
    }

    const records: { userId: string; approveId: string; creator: string; date: string; instanceIds: string[] }[] = [];
    for (const workDate of workDates.values()) {
      const resp = await this.attendanceService.getApproveRecords(workDate.userId, workDate.date);
      if (resp.errcode !== 0) {
        records.push({
          userId: workDate.userId,
          approveId: `QUERY_FAILED:${resp.errcode}`,
          creator: workDate.creator,
          date: workDate.date,
          instanceIds: workDate.instanceIds,
        });
        continue;
      }

      const approveList = resp.result?.approve_list || [];
      for (const approval of approveList) {
        const approveId = this.getAttendanceApproveId(approval);
        if (!approveId || !approveId.startsWith('undefined-')) continue;
        if (approval.tag_name !== '外出') continue;

        records.push({
          userId: workDate.userId,
          approveId,
          creator: workDate.creator,
          date: workDate.date,
          instanceIds: workDate.instanceIds,
        });
      }
    }

    const unique = new Map<string, { userId: string; approveId: string; creator: string; date: string; instanceIds: string[] }>();
    for (const record of records) {
      if (!record.approveId.startsWith('undefined-')) continue;
      const key = `${record.userId}|${record.approveId}`;
      const existing = unique.get(key);
      if (existing) {
        for (const instanceId of record.instanceIds) {
          if (!existing.instanceIds.includes(instanceId)) existing.instanceIds.push(instanceId);
        }
        continue;
      }
      unique.set(key, record);
    }
    const deduped = [...unique.values()];

    if (body.dryRun !== false) {
      return {
        dryRun: true,
        scannedDays: workDates.size,
        total: deduped.length,
        records: deduped,
      };
    }

    const results: any[] = [];
    let success = 0;
    let failed = 0;

    for (const record of deduped) {
      try {
        const resp = await this.attendanceService.approveCancel(record.userId, record.approveId);
        const ok = resp.errcode === 0;
        if (ok) success++;
        else failed++;
        results.push({
          ...record,
          status: ok ? 'CANCELLED' : 'FAILED',
          response: resp,
        });
      } catch (error: any) {
        failed++;
        results.push({
          ...record,
          status: 'ERROR',
          error: error.message,
        });
      }
    }

    return {
      dryRun: false,
      scannedDays: workDates.size,
      total: deduped.length,
      success,
      failed,
      results,
    };
  }

  /**
   * POST /api/v1/organization/dingtalk/debug/approve-finish
   * 临时测试：手动写入一条考勤审批记录
   */
  @Post('debug/approve-finish')
  async debugApproveFinish(@Body() body: any) {
    const resp = await this.attendanceService.approveFinish(body);
    return resp;
  }

  /**
   * GET /api/v1/organization/dingtalk/debug/trip-overlap
   * 检查指定时间段内是否有员工同一天出现多条出差申请
   * 排除已被出差变更单取消或变更的原始申请
   */
  @Get('debug/trip-overlap')
  async debugTripOverlap(@Query('from') from?: string, @Query('to') to?: string) {
    // 使用请求参数或默认取当前季度
    const now = new Date();
    const quarterStart = new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 1);
    const quarterEnd = new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3 + 3, 0);
    const filterFrom = from || quarterStart.toISOString().split('T')[0];
    const filterTo = to || quarterEnd.toISOString().split('T')[0];

    // 1. 获取出差变更单，找出所有被取消/变更的原始申请单号
    const changeData = await this.yidaService.searchForm(
      FORM_UUIDS.BUSINESS_TRIP_CHANGE,
      APPROVED_SEARCH_CONDITION,
    );
    const cancelledSerialNos = new Set<string>();
    for (const change of changeData) {
      const fd = change.formData;
      const changeType = fd[BUSINESS_TRIP_CHANGE_FIELDS.TYPE];
      // 所有变更类型都会取消原单的考勤
      const assocFieldMap: Record<string, string> = {
        [BUSINESS_TRIP_CHANGE_TYPES.CANCEL_UNCHANGED]: BUSINESS_TRIP_CHANGE_FIELDS.CANCEL_UNCHANGED_ASSOC,
        [BUSINESS_TRIP_CHANGE_TYPES.CANCEL_CHANGED]: BUSINESS_TRIP_CHANGE_FIELDS.CANCEL_CHANGED_ASSOC,
        [BUSINESS_TRIP_CHANGE_TYPES.FIRST_CHANGE]: BUSINESS_TRIP_CHANGE_FIELDS.FIRST_CHANGE_ASSOC,
        [BUSINESS_TRIP_CHANGE_TYPES.NOT_FIRST_CHANGE]: BUSINESS_TRIP_CHANGE_FIELDS.NOT_FIRST_CHANGE_ASSOC,
      };
      const assocField = assocFieldMap[changeType];
      if (assocField && fd[assocField]) {
        try {
          const assoc = JSON.parse(JSON.parse(fd[assocField]));
          if (assoc?.[0]?.title) {
            cancelledSerialNos.add(assoc[0].title);
          }
        } catch {}
      }
    }

    // 2. 获取出差申请
    const data = await this.yidaService.searchForm(
      FORM_UUIDS.BUSINESS_TRIP,
      APPROVED_SEARCH_CONDITION,
      from,
      to,
    );

    // 3. 展开每个申请的每段行程为逐天记录（排除已被取消/变更的）
    const dayRecords: { userId: string; name: string; date: string; serialNo: string; fromDate: string; toDate: string }[] = [];

    for (const item of data) {
      const serialNo = getDingtalkSerialNumber(item) || 'unknown';
      if (cancelledSerialNos.has(serialNo)) continue; // 跳过已被取消/变更的

      const formData = item.formData;
      const creator = formData[BUSINESS_TRIP_FIELDS.CREATOR] || 'unknown';
      const userId = item.creatorUserId;
      const travelTimes = formData[BUSINESS_TRIP_FIELDS.TABLE_FIELD] || [];

      for (const seg of travelTimes) {
        const fromTs = seg[BUSINESS_TRIP_FIELDS.FROM_DATE];
        const toTs = seg[BUSINESS_TRIP_FIELDS.TO_DATE];
        if (!fromTs || !toTs) continue;

        const fromDateStr = this.formatDateCST(fromTs);
        const toDateStr = this.formatDateCST(toTs);

        const cur = new Date(fromDateStr);
        const end = new Date(toDateStr);
        while (cur <= end) {
          const dateStr = cur.toISOString().split('T')[0];
          if ((!filterFrom || dateStr >= filterFrom) && (!filterTo || dateStr <= filterTo)) {
            dayRecords.push({ userId, name: creator, date: dateStr, serialNo, fromDate: fromDateStr, toDate: toDateStr });
          }
          cur.setDate(cur.getDate() + 1);
        }
      }
    }

    // 4. 按 userId+date 分组，找出同一天有多条申请的
    const grouped: Record<string, typeof dayRecords> = {};
    for (const r of dayRecords) {
      const key = `${r.userId}|${r.date}`;
      if (!grouped[key]) grouped[key] = [];
      grouped[key].push(r);
    }

    const overlaps = Object.entries(grouped)
      .filter(([, records]) => {
        const uniqueSerials = new Set(records.map(r => r.serialNo));
        return uniqueSerials.size > 1;
      })
      .map(([, records]) => {
        // 去重申请单号
        const seen = new Set<string>();
        const uniqueApps: { serialNo: string; range: string }[] = [];
        for (const r of records) {
          if (!seen.has(r.serialNo)) {
            seen.add(r.serialNo);
            uniqueApps.push({ serialNo: r.serialNo, range: `${r.fromDate} ~ ${r.toDate}` });
          }
        }
        return {
          userId: records[0].userId,
          name: records[0].name,
          date: records[0].date,
          applications: uniqueApps,
        };
      })
      .sort((a, b) => a.name.localeCompare(b.name) || a.date.localeCompare(b.date));

    return {
      totalApplications: data.length,
      cancelledByChange: cancelledSerialNos.size,
      activeApplications: data.length - [...cancelledSerialNos].filter(s => data.some(d => getDingtalkSerialNumber(d) === s)).length,
      totalDayRecords: dayRecords.length,
      overlapCount: overlaps.length,
      overlaps,
    };
  }

  /**
   * POST /api/v1/organization/dingtalk/debug/batch-cancel-trips
   * 批量取消指定日期之后的所有出差考勤记录
   */
  @Post('debug/batch-cancel-trips')
  async debugBatchCancelTrips(@Body() body: { since?: string; dryRun?: boolean }) {
    const since = body.since || '2026-01-01';
    const sinceTs = new Date(since + 'T00:00:00+08:00').getTime();

    // 1. 获取所有出差申请
    const tripData = await this.yidaService.searchForm(
      FORM_UUIDS.BUSINESS_TRIP,
      APPROVED_SEARCH_CONDITION,
    );

    // 2. 获取所有出差变更单（非取消类型的变更会写入新 approveId）
    const changeData = await this.yidaService.searchForm(
      FORM_UUIDS.BUSINESS_TRIP_CHANGE,
      APPROVED_SEARCH_CONDITION,
    );

    const cancelRecords: { userId: string; approveId: string; name: string; fromDate: string; toDate: string }[] = [];

    // 处理出差申请
    for (const item of tripData) {
      const serialNo = getDingtalkSerialNumber(item);
      if (!serialNo) continue;
      const userId = item.creatorUserId;
      const name = item.formData[BUSINESS_TRIP_FIELDS.CREATOR] || 'unknown';
      const travelTimes = item.formData[BUSINESS_TRIP_FIELDS.TABLE_FIELD] || [];

      let index = 1;
      for (const seg of travelTimes) {
        const fromTs = seg[BUSINESS_TRIP_FIELDS.FROM_DATE];
        const toTs = seg[BUSINESS_TRIP_FIELDS.TO_DATE];
        if (!fromTs || !toTs) { index++; continue; }
        // 只处理 since 之后的行程段
        if (toTs >= sinceTs) {
          const approveId = `${serialNo}-${index}`;
          cancelRecords.push({
            userId, approveId, name,
            fromDate: this.formatDateCST(fromTs),
            toDate: this.formatDateCST(toTs),
          });
        }
        index++;
      }
    }

    // 处理出差变更单（非取消类型写入了新的 approveId）
    for (const item of changeData) {
      const serialNo = getDingtalkSerialNumber(item);
      if (!serialNo) continue;
      const fd = item.formData;
      const changeType = fd[BUSINESS_TRIP_CHANGE_FIELDS.TYPE];
      const userId = item.creatorUserId;
      const name = fd[BUSINESS_TRIP_CHANGE_FIELDS.CREATOR] || 'unknown';

      // 取消类型不写入新 approveId，跳过
      if (changeType === BUSINESS_TRIP_CHANGE_TYPES.CANCEL_UNCHANGED ||
          changeType === BUSINESS_TRIP_CHANGE_TYPES.CANCEL_CHANGED) continue;

      const travelTimes = fd[BUSINESS_TRIP_CHANGE_FIELDS.TABLE_FIELD] || [];
      let index = 1;
      for (const seg of travelTimes) {
        const fromTs = seg[BUSINESS_TRIP_CHANGE_FIELDS.FROM_DATE];
        const toTs = seg[BUSINESS_TRIP_CHANGE_FIELDS.TO_DATE];
        if (!fromTs || !toTs) { index++; continue; }
        if (toTs >= sinceTs) {
          const approveId = `${serialNo}-${index}`;
          cancelRecords.push({
            userId, approveId, name,
            fromDate: this.formatDateCST(fromTs),
            toDate: this.formatDateCST(toTs),
          });
        }
        index++;
      }
    }

    // 去重
    const uniqueMap = new Map<string, typeof cancelRecords[0]>();
    for (const r of cancelRecords) {
      const key = `${r.userId}|${r.approveId}`;
      if (!uniqueMap.has(key)) uniqueMap.set(key, r);
    }
    const uniqueRecords = [...uniqueMap.values()];

    if (body.dryRun) {
      return {
        dryRun: true,
        since,
        totalToCancel: uniqueRecords.length,
        records: uniqueRecords.map(r => ({
          name: r.name, userId: r.userId, approveId: r.approveId,
          range: `${r.fromDate} ~ ${r.toDate}`,
        })),
      };
    }

    // 实际取消
    const results: any[] = [];
    let successCount = 0;
    let failCount = 0;
    for (const record of uniqueRecords) {
      try {
        const resp = await this.attendanceService.approveCancel(record.userId, record.approveId);
        if (resp.errcode === 0) {
          successCount++;
        } else {
          failCount++;
        }
        results.push({
          name: record.name, userId: record.userId, approveId: record.approveId,
          status: resp.errcode === 0 ? 'CANCELLED' : 'FAILED',
          errcode: resp.errcode,
        });
      } catch (error: any) {
        failCount++;
        results.push({
          name: record.name, userId: record.userId, approveId: record.approveId,
          status: 'ERROR', error: error.message,
        });
      }
    }

    return { since, total: uniqueRecords.length, success: successCount, failed: failCount, results };
  }

  private formatDateCST(timestamp: number): string {
    const formatter = new Intl.DateTimeFormat('sv-SE', { timeZone: 'Asia/Shanghai' });
    return formatter.format(new Date(timestamp));
  }

  private listCoveredDates(fromTimestamp: number, toTimestamp: number): string[] {
    const dates = new Set<string>();
    const current = new Date(fromTimestamp);
    const end = new Date(toTimestamp);

    while (current.getTime() <= end.getTime()) {
      dates.add(this.formatDateCST(current.getTime()));
      current.setUTCDate(current.getUTCDate() + 1);
      current.setUTCHours(0, 0, 0, 0);
    }

    return [...dates];
  }

  private getAttendanceApproveId(item: any): string {
    return item?.procInst_id || item?.approve_id || item?.approveId || '';
  }

  /**
   * GET /api/v1/organization/dingtalk/debug/yida-sample
   * 查看宜搭表单的数据结构
   * ?type=trip (默认) | change
   */
  @Get('debug/yida-sample')
  async debugYidaSample(
    @Query('type') type?: string,
    @Query('serialNo') serialNo?: string,
    @Query('from') fromTime?: string,
    @Query('to') toTime?: string,
  ) {
    const formUuid = type === 'change' ? FORM_UUIDS.BUSINESS_TRIP_CHANGE : FORM_UUIDS.BUSINESS_TRIP;
    // 如果指定了 serialNo，同时搜索审批通过和全部数据
    const data = await this.yidaService.searchForm(formUuid, APPROVED_SEARCH_CONDITION, fromTime, toTime);

    if (serialNo) {
      // 先在审批通过的数据中搜
      const findIn = (list: any[]) => list.find(d =>
        getDingtalkSerialNumber(d) === serialNo ||
        d.formData?.[BUSINESS_TRIP_FIELDS.NUMBER] === serialNo,
      );
      let found = findIn(data);
      let source = 'approved';

      if (!found) {
        // 不带过滤条件搜全部数据
        const allData = await this.yidaService.searchForm(formUuid, '[]');
        found = findIn(allData);
        source = 'all';
        if (!found) {
          // 查看所有 serialNo 范围
          const allSerials = allData.map(d => getDingtalkSerialNumber(d)).filter(Boolean).sort();
          const firstSerial = allSerials[0];
          const lastSerial = allSerials[allSerials.length - 1];
          // 按创建者姓名模糊搜
          const keyword = serialNo;
          const byName = allData.filter(d => {
            const creator = d.formData?.[BUSINESS_TRIP_FIELDS.CREATOR] || '';
            const num = d.formData?.[BUSINESS_TRIP_FIELDS.NUMBER] || '';
            return creator.includes(keyword) || num.includes(keyword);
          }).map(d => ({
            serialNo: getDingtalkSerialNumber(d),
            number: d.formData?.[BUSINESS_TRIP_FIELDS.NUMBER],
            creator: d.formData?.[BUSINESS_TRIP_FIELDS.CREATOR],
            userId: d.creatorUserId,
          }));
          return { message: `未找到 ${serialNo}`, approvedCount: data.length, allCount: allData.length, serialRange: { first: firstSerial, last: lastSerial }, byNameOrNumber: byName };
        }
      }
      const travelTimes = found.formData?.[BUSINESS_TRIP_FIELDS.TABLE_FIELD] || [];
      const segments = travelTimes.map((seg: any, i: number) => ({
        index: i + 1,
        fromDate: seg[BUSINESS_TRIP_FIELDS.FROM_DATE] ? this.formatDateCST(seg[BUSINESS_TRIP_FIELDS.FROM_DATE]) : null,
        toDate: seg[BUSINESS_TRIP_FIELDS.TO_DATE] ? this.formatDateCST(seg[BUSINESS_TRIP_FIELDS.TO_DATE]) : null,
      }));
      return {
        source,
        serialNo: getDingtalkSerialNumber(found),
        creator: found.formData?.[BUSINESS_TRIP_FIELDS.CREATOR],
        userId: found.creatorUserId,
        number: found.formData?.[BUSINESS_TRIP_FIELDS.NUMBER],
        segments,
      };
    }

    if (data.length === 0) return { message: '无数据' };

    // 去重列出所有申请（按 serialNo 去重）
    const seen = new Set<string>();
    const records: any[] = [];
    for (const d of data) {
      const serialNo = getDingtalkSerialNumber(d);
      if (!serialNo || seen.has(serialNo)) continue;
      seen.add(serialNo);
      const travelTimes = d.formData?.[BUSINESS_TRIP_FIELDS.TABLE_FIELD] || [];
      const segments = travelTimes.map((seg: any, i: number) => {
        const from = seg[BUSINESS_TRIP_FIELDS.FROM_DATE] ? this.formatDateCST(seg[BUSINESS_TRIP_FIELDS.FROM_DATE]) : null;
        const to = seg[BUSINESS_TRIP_FIELDS.TO_DATE] ? this.formatDateCST(seg[BUSINESS_TRIP_FIELDS.TO_DATE]) : null;
        return `${from} ~ ${to}`;
      });
      records.push({
        serialNo,
        creator: d.formData?.[BUSINESS_TRIP_FIELDS.CREATOR],
        userId: d.creatorUserId,
        segments,
      });
    }
    records.sort((a, b) => a.serialNo.localeCompare(b.serialNo));

    return { totalRecords: data.length, uniqueApplications: records.length, records };
  }

  // ==================== SAP 采购同步 ====================

  /**
   * GET /api/v1/organization/dingtalk/sap-purchases/list
   * 查询采购申请列表（从本地缓存 + 同步状态）
   */
  @Get('sap-purchases/list')
  async getSapPurchaseList(
    @Query('sapEnv') sapEnv?: string,
    @Query('keyword') keyword?: string,
    @Query('status') status?: string,
  ) {
    const env: SapEnvironment = sapEnv === 'test' ? 'test' : 'production';
    return this.purchaseSapSyncService.listPurchases(env, keyword, status);
  }

  /**
   * POST /api/v1/organization/dingtalk/sap-purchases/refresh
   * 从宜搭增量拉取采购申请到本地缓存
   */
  @Post('sap-purchases/refresh')
  async refreshSapPurchaseCache(
    @Body() body: { sapEnv?: 'test' | 'production' },
  ) {
    const env: SapEnvironment = body.sapEnv === 'test' ? 'test' : 'production';
    return this.purchaseSapSyncService.refreshPurchaseCache(env);
  }

  /**
   * GET /api/v1/organization/dingtalk/sap-purchases/records
   * 查询同步记录
   */
  @Get('sap-purchases/records')
  async getSapPurchaseRecords(
    @Query('page') page?: string,
    @Query('limit') limit?: string,
    @Query('status') status?: string,
  ) {
    return this.purchaseSapSyncService.getRecords(
      parseInt(page || '1', 10),
      parseInt(limit || '20', 10),
      status,
    );
  }

  /**
   * POST /api/v1/organization/dingtalk/sap-purchases/sync
   * 手动同步指定采购单到 SAP
   */
  @Post('sap-purchases/sync')
  async syncSapPurchases(
    @Body() body: {
      serialNumbers: string[];
      sapEnv: 'test' | 'production';
    },
  ) {
    const results = await this.purchaseSapSyncService.syncBatch(body.serialNumbers, body.sapEnv);
    const successCount = results.filter(r => r.success).length;
    return {
      success: successCount === results.length,
      total: results.length,
      successCount,
      failedCount: results.length - successCount,
      results,
    };
  }
}
