import {
  BadRequestException,
  Body,
  Controller,
  ForbiddenException,
  Get,
  Post,
  Query,
  Request,
  UnauthorizedException,
  UseGuards,
} from '@nestjs/common';
import type { Request as ExpressRequest } from 'express';
import { JwtAuthGuard } from '@modules/organization/auth/guards/jwt-auth.guard';
import { PrismaService } from '@core/database/prisma/prisma.service';
import {
  getMeetingRoleFromUser,
  isMeetingAdminRole,
} from '@modules/meeting-attendance/utils/meeting-roles';
import { MeetingAttendanceAuditLogWriter } from '@modules/meeting-attendance/services/audit-log-writer.service';
import {
  MEETING_ATTENDANCE_AUDIT_ACTIONS,
  MEETING_ATTENDANCE_AUDIT_RESOURCES,
} from '@modules/meeting-attendance/constants/audit';
import { AdpSchedulerService, ADP_TASK_CODES } from './adp-scheduler.service';

const DEFAULT_PAGE_SIZE = 50;
const MAX_PAGE_SIZE = 200;
const MAX_LOOKBACK_DAYS = 30;
const MAX_LOOKAHEAD_DAYS = 60;

@Controller('adp-sync')
@UseGuards(JwtAuthGuard)
export class AdpSyncController {
  constructor(
    private readonly scheduler: AdpSchedulerService,
    private readonly prisma: PrismaService,
    private readonly auditLogWriter: MeetingAttendanceAuditLogWriter,
  ) {}

  @Post('linker/trigger')
  async triggerLinker(
    @Body() body: { dryRun?: boolean } | undefined,
    @Request() req: ExpressRequest,
  ) {
    const actor = this.requireMeetingAdmin(req);
    const r = this.scheduler.triggerLinkerAsync(actor.userId ?? actor.id, body?.dryRun ?? false);
    return { accepted: r.accepted, reason: r.reason, status: 'RUNNING' };
  }

  @Post('pto/trigger')
  async triggerPtoSync(
    @Body() body: { windowStartDays?: number; windowEndDays?: number } | undefined,
    @Request() req: ExpressRequest,
  ) {
    const actor = this.requireMeetingAdmin(req);
    const r = this.scheduler.triggerPtoSyncAsync(
      actor.userId ?? actor.id,
      body?.windowStartDays,
      body?.windowEndDays,
    );
    return { accepted: r.accepted, reason: r.reason, status: 'RUNNING' };
  }

  @Get('status')
  async getStatus(@Request() req: ExpressRequest) {
    this.requireMeetingAdmin(req);
    return this.scheduler.getStatus();
  }

  @Get('admin/pto-schedules')
  async listPtoSchedules(
    @Query() query: Record<string, any>,
    @Request() req: ExpressRequest,
  ) {
    const actor = this.requireMeetingAdmin(req);

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

    const defaultStart = new Date(today.getUTCFullYear(), today.getUTCMonth(), 1);
    const defaultEnd = new Date(today.getUTCFullYear(), today.getUTCMonth() + 2, 0);

    const startDate = query.startDate ? new Date(query.startDate) : defaultStart;
    const endDate = query.endDate ? new Date(query.endDate) : defaultEnd;

    const minStart = new Date(today);
    minStart.setUTCDate(minStart.getUTCDate() - MAX_LOOKBACK_DAYS);
    const maxEnd = new Date(today);
    maxEnd.setUTCDate(maxEnd.getUTCDate() + MAX_LOOKAHEAD_DAYS);
    if (startDate < minStart || endDate > maxEnd || startDate > endDate) {
      throw new BadRequestException({
        error: 'INVALID_DATE_RANGE',
        message: `Allowed range: today-${MAX_LOOKBACK_DAYS}d to today+${MAX_LOOKAHEAD_DAYS}d`,
      });
    }

    const page = Math.max(1, parseInt(String(query.page ?? 1), 10));
    const pageSize = Math.min(
      MAX_PAGE_SIZE,
      Math.max(1, parseInt(String(query.pageSize ?? DEFAULT_PAGE_SIZE), 10)),
    );

    const userFilter: any = {};
    if (query.userId) {
      userFilter.userId = String(query.userId);
    } else if (query.q) {
      const q = String(query.q).trim();
      if (q) {
        userFilter.user = {
          OR: [
            { displayName: { contains: q, mode: 'insensitive' } },
            { email: { contains: q, mode: 'insensitive' } },
          ],
        };
      }
    }

    if (query.departmentId) {
      userFilter.user = {
        ...(userFilter.user ?? {}),
        departmentMemberships: {
          some: { departmentId: String(query.departmentId), leftAt: null },
        },
      };
    }

    const where: any = {
      leaveDate: { gte: startDate, lte: endDate },
      ...userFilter,
    };

    const [items, total] = await Promise.all([
      this.prisma.adpPtoSchedule.findMany({
        where,
        orderBy: [{ leaveDate: 'asc' }, { startTime: 'asc' }],
        skip: (page - 1) * pageSize,
        take: pageSize,
        select: {
          id: true,
          leaveDate: true,
          startTime: true,
          endTime: true,
          syncedAt: true,
          user: { select: { id: true, displayName: true, email: true } },
        },
      }),
      this.prisma.adpPtoSchedule.count({ where }),
    ]);

    await this.auditLogWriter.log({
      request: req,
      actor: actor as any,
      action: MEETING_ATTENDANCE_AUDIT_ACTIONS.ADP_PTO_DATA_ACCESS,
      resource: MEETING_ATTENDANCE_AUDIT_RESOURCES.ATTENDANCE,
      statusCode: 200,
      requestBody: {
        startDate: startDate.toISOString(),
        endDate: endDate.toISOString(),
        userId: query.userId,
        departmentId: query.departmentId,
        q: query.q,
        page,
        pageSize,
      },
      changes: {
        accessType: 'READ_PTO_LIST',
        resultCount: items.length,
        totalMatched: total,
      },
    });

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

  // ============ 手动匹配（ADP 员工 ↔ FFAI User）============

  /** 拉最近一次 LINKER 执行写入的 unmatchedAdp 列表 */
  @Get('admin/unmatched-adp')
  async listUnmatchedAdp(@Request() req: ExpressRequest) {
    this.requireMeetingAdmin(req);

    const task = await this.prisma.automationTask.findFirst({
      where: { code: ADP_TASK_CODES.LINKER },
      select: { id: true },
    });
    if (!task) return { items: [], lastRunAt: null };

    const exec = await this.prisma.automationExecution.findFirst({
      where: { taskId: task.id, status: 'SUCCESS' },
      orderBy: { startedAt: 'desc' },
      select: { startedAt: true, result: true },
    });
    const result = (exec?.result ?? {}) as { unmatchedAdp?: Array<{ aoid: string; email: string }> };
    return {
      items: result.unmatchedAdp ?? [],
      lastRunAt: exec?.startedAt ?? null,
    };
  }

  /** 搜未绑定 aoid 的 FFAI User（按 displayName / email 模糊） */
  @Get('admin/users-search')
  async searchUnlinkedUsers(
    @Query('q') q: string,
    @Request() req: ExpressRequest,
  ) {
    this.requireMeetingAdmin(req);
    const term = (q || '').trim();
    if (term.length < 2) return { items: [] };

    const items = await this.prisma.user.findMany({
      where: {
        status: 'ACTIVE',
        adpAoid: null,
        OR: [
          { displayName: { contains: term, mode: 'insensitive' } },
          { email: { contains: term, mode: 'insensitive' } },
        ],
      },
      select: { id: true, displayName: true, email: true },
      take: 20,
      orderBy: { displayName: 'asc' },
    });
    return { items };
  }

  /** 手动绑定：把指定 aoid 写到指定 user.adpAoid */
  @Post('admin/manual-link')
  async manualLink(
    @Body() body: { aoid: string; userId: string },
    @Request() req: ExpressRequest,
  ) {
    const actor = this.requireMeetingAdmin(req);
    const aoid = (body?.aoid || '').trim();
    const userId = (body?.userId || '').trim();
    if (!aoid || !userId) {
      throw new BadRequestException('aoid 和 userId 必填');
    }

    const target = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { id: true, displayName: true, email: true, adpAoid: true, status: true },
    });
    if (!target) throw new BadRequestException('User 不存在');
    if (target.status !== 'ACTIVE') throw new BadRequestException('User 非 ACTIVE');
    if (target.adpAoid && target.adpAoid !== aoid) {
      throw new BadRequestException(`User 已绑定其他 aoid: ${target.adpAoid}`);
    }

    const conflict = await this.prisma.user.findFirst({
      where: { adpAoid: aoid, NOT: { id: userId } },
      select: { id: true, email: true },
    });
    if (conflict) {
      throw new BadRequestException(`aoid ${aoid} 已绑定给 ${conflict.email}`);
    }

    const updated = await this.prisma.user.update({
      where: { id: userId },
      data: { adpAoid: aoid, adpLinkedAt: new Date() },
      select: { id: true, displayName: true, email: true, adpAoid: true, adpLinkedAt: true },
    });

    await this.auditLogWriter.log({
      request: req,
      actor: actor as any,
      action: MEETING_ATTENDANCE_AUDIT_ACTIONS.ADP_PTO_DATA_ACCESS,
      resource: MEETING_ATTENDANCE_AUDIT_RESOURCES.ATTENDANCE,
      statusCode: 200,
      requestBody: { aoid, userId },
      changes: {
        accessType: 'MANUAL_LINK_AOID',
        userId: updated.id,
        userEmail: updated.email,
        aoid,
      },
    });

    return { success: true, user: updated };
  }

  private requireMeetingAdmin(req: ExpressRequest) {
    const user = req.user as
      | { userId?: string; id?: string; email?: string; displayName?: string; roles?: any[] }
      | undefined;
    if (!user?.userId && !user?.id) {
      throw new UnauthorizedException('Unauthorized');
    }
    const role = getMeetingRoleFromUser(user as any);
    if (!isMeetingAdminRole(role)) {
      throw new ForbiddenException(
        'Insufficient permissions. Only meeting administrators can access ADP sync.',
      );
    }
    return user;
  }
}
