/**
 * v1.3 Outlook 同步参会人自动入组 - 编排服务
 *
 * 责任：
 * - 接 outlook-sync syncAttendees 在 "邮箱不在 users 表" 分支的 hook 调用
 * - 查 Graph 用户、跑识别规则、调 Graph 加组
 * - 写 audit_logs（actor=itadmin，source=OUTLOOK_AUTO_SYNC）
 * - 任何异常都吞掉 + logger.warn，绝不抛给上游 sync 主流程
 *
 * 详见 docs/modules/meeting-attendance/01-prd.md "自动入组真人参会人（v1.3 新增）"。
 */
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { EntraService, EntraUser } from '../../organization/entra/entra.service';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { ADMINISTRATOR_ROLE_CODE } from '@common/utils/role-check.util';
import { MeetingAttendanceAuditLogWriter } from './audit-log-writer.service';
import {
  MEETING_ATTENDANCE_AUDIT_ACTIONS,
  MEETING_ATTENDANCE_AUDIT_RESOURCES,
} from '../constants/audit';
import { evaluateGraphUserForAutoAdd } from './outlook-attendee-auto-add-filter';

const SYSTEM_ACTOR_USERNAME = 'itadmin';
const AUDIT_SOURCE = 'OUTLOOK_AUTO_SYNC';
const DEDUP_TTL_MS = 10 * 60 * 1000;
const DEDUP_MAX_ENTRIES = 5000;
const FAN_OUT_CHUNK_SIZE = 5;

type AutoAddAuditDecision =
  | 'added'
  | 'skipped_disabled'
  | 'skipped_guest'
  | 'skipped_no_mail'
  | 'skipped_email_mismatch'
  | 'skipped_resource_naming'
  | 'skipped_user_not_found'
  | 'failed';

interface SystemActor {
  id: string;
  email: string;
  username: string;
}

@Injectable()
export class OutlookAttendeeAutoAddService {
  private readonly logger = new Logger(OutlookAttendeeAutoAddService.name);
  private cachedSystemActor: SystemActor | null = null;
  // dedup：Outlook 同步频繁触发，同邮箱 N 个 meeting 都会进 hook；TTL 内只跑一次省 Graph + audit
  private readonly recentAttempts = new Map<string, number>();

  constructor(
    private readonly entraService: EntraService,
    private readonly configService: ConfigService,
    private readonly prisma: PrismaService,
    private readonly auditLogWriter: MeetingAttendanceAuditLogWriter,
  ) {}

  /**
   * 批量触发自动入组：限并发 chunk + dedup。供 outlook-sync.syncAttendees 事务后调用。
   * 内部 Promise.allSettled 不抛错，调用方可 fire-and-forget 但建议 .catch 兜底未预期路径。
   */
  async fireAutoAddBatched(emails: Iterable<string>): Promise<void> {
    const arr = Array.from(emails);
    for (let i = 0; i < arr.length; i += FAN_OUT_CHUNK_SIZE) {
      const chunk = arr.slice(i, i + FAN_OUT_CHUNK_SIZE);
      await Promise.allSettled(chunk.map((email) => this.tryAutoAddAttendee(email)));
    }
  }

  /**
   * 单邮箱入口（也是 fireAutoAddBatched 内部调）。任何异常都不抛给调用方。
   *
   * @param email 同步进来的参会人邮箱（已 trim+lowercase）
   */
  async tryAutoAddAttendee(email: string): Promise<void> {
    if (this.shouldSkipDuplicate(email)) {
      return;
    }
    try {
      await this.runAutoAdd(email);
    } catch (error: any) {
      this.logger.warn(
        `[outlook-auto-add] unexpected error for ${email}: ${error?.message ?? error}`,
      );
    }
  }

  private shouldSkipDuplicate(email: string): boolean {
    const now = Date.now();
    const last = this.recentAttempts.get(email);
    if (last !== undefined && now - last < DEDUP_TTL_MS) {
      return true;
    }
    this.recentAttempts.set(email, now);
    if (this.recentAttempts.size > DEDUP_MAX_ENTRIES) {
      for (const [k, t] of this.recentAttempts) {
        if (now - t >= DEDUP_TTL_MS) this.recentAttempts.delete(k);
      }
    }
    return false;
  }

  private async runAutoAdd(email: string): Promise<void> {
    if (!this.entraService.isEnabled()) {
      this.logger.debug(`[outlook-auto-add] Entra 未配置，跳过 ${email}`);
      return;
    }
    const groupId = this.entraService.getSyncGroupId();
    if (!groupId) {
      this.logger.debug(`[outlook-auto-add] AZURE_ENTRA_SYNC_GROUP_ID 未配置，跳过 ${email}`);
      return;
    }

    let graphUser: EntraUser | null;
    try {
      graphUser = await this.entraService.getUserByEmail(email);
    } catch (error: any) {
      const readableMessage = error?.code ?? error?.message ?? String(error);
      const graphStatus = error?.details?.statusCode ?? error?.statusCode;
      await this.writeAudit({
        email,
        decision: 'failed',
        graphResponseStatus: graphStatus,
        errorMessage: `getUserByEmail: ${readableMessage}`,
      });
      this.logger.warn(
        `[outlook-auto-add] getUserByEmail failed for ${email}: ${readableMessage} (status=${graphStatus})`,
      );
      return;
    }

    if (!graphUser) {
      await this.writeAudit({ email, decision: 'skipped_user_not_found' });
      this.logger.log(`[outlook-auto-add] ${email} not found in Graph, skipped`);
      return;
    }

    const filterResult = evaluateGraphUserForAutoAdd(graphUser, email);
    if (filterResult.decision !== 'eligible') {
      await this.writeAudit({
        email,
        decision: filterResult.decision,
        matchedRule: filterResult.matchedRule,
        graphUserId: graphUser.id,
      });
      this.logger.log(
        `[outlook-auto-add] ${email} skipped: ${filterResult.decision}` +
          (filterResult.matchedRule ? ` (${filterResult.matchedRule})` : ''),
      );
      return;
    }

    try {
      const result = await this.entraService.addUserToGroup(groupId, graphUser.id);
      await this.writeAudit({
        email,
        decision: 'added',
        graphUserId: graphUser.id,
        graphResponseStatus: result.alreadyMember ? 400 : 204,
        extras: result.alreadyMember ? { alreadyMember: true } : undefined,
      });
      this.logger.log(
        `[outlook-auto-add] ${email} added to group ${groupId}` +
          (result.alreadyMember ? ' (already member)' : ''),
      );
    } catch (error: any) {
      // BusinessException：真实 Graph 状态码在 details.statusCode；可读 message 在 .code 字段
      const graphStatus = error?.details?.statusCode ?? error?.statusCode;
      const readableMessage = error?.code ?? error?.message ?? String(error);
      await this.writeAudit({
        email,
        decision: 'failed',
        graphUserId: graphUser.id,
        graphResponseStatus: graphStatus,
        errorMessage: `addUserToGroup: ${readableMessage}`,
      });
      this.logger.warn(
        `[outlook-auto-add] addUserToGroup failed for ${email} (${graphUser.id}): ${readableMessage} (status=${graphStatus})`,
      );
    }
  }

  private async writeAudit(params: {
    email: string;
    decision: AutoAddAuditDecision;
    matchedRule?: string;
    graphUserId?: string;
    graphResponseStatus?: number;
    errorMessage?: string;
    extras?: Record<string, unknown>;
  }): Promise<void> {
    const actor = await this.getSystemActor();
    if (!actor) {
      this.logger.warn(
        `[outlook-auto-add] system actor (username=${SYSTEM_ACTOR_USERNAME}) not found in users table; audit skipped for ${params.email}`,
      );
      return;
    }

    await this.auditLogWriter.log({
      actor: {
        userId: actor.id,
        id: actor.id,
        email: actor.email,
        username: actor.username,
        displayName: actor.username,
        role: ADMINISTRATOR_ROLE_CODE,
      },
      action: MEETING_ATTENDANCE_AUDIT_ACTIONS.OUTLOOK_ATTENDEE_AUTO_ADD,
      resource: MEETING_ATTENDANCE_AUDIT_RESOURCES.USER,
      statusCode: params.decision === 'failed' ? 500 : 200,
      resourceId: params.graphUserId,
      source: AUDIT_SOURCE,
      changes: {
        email: params.email,
        decision: params.decision,
        ...(params.matchedRule ? { matchedRule: params.matchedRule } : {}),
        ...(params.graphUserId ? { graphUserId: params.graphUserId } : {}),
        ...(typeof params.graphResponseStatus === 'number'
          ? { graphResponseStatus: params.graphResponseStatus }
          : {}),
        ...(params.extras ?? {}),
      },
      errorMessage: params.errorMessage,
    });
  }

  private async getSystemActor(): Promise<SystemActor | null> {
    if (this.cachedSystemActor) {
      return this.cachedSystemActor;
    }
    const row = await this.prisma.user.findFirst({
      where: { username: SYSTEM_ACTOR_USERNAME, deletedAt: null },
      select: { id: true, email: true, username: true },
    });
    if (!row) return null;
    this.cachedSystemActor = {
      id: row.id,
      email: row.email,
      username: row.username,
    };
    return this.cachedSystemActor;
  }
}
