import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { Prisma, M365UserActivityChangeField } from '@prisma/client';
import { SkipAssertAccess } from '@common/decorators/skip-assert-access.decorator';
import { GRAPH_CLIENT_TOKEN } from '../graph/graph-client.interface';
import type {
  GraphClient,
  GraphUser,
  ActivityReportName,
} from '../graph/graph-client.interface';
import { resolveLicenseDisplayName } from '../graph/sku-display-names';
import { SyncInProgressException } from '../m365-dormant.exceptions';
import { ExecutionResultPayload, InactiveDistribution } from '../dto/m365-dormant.dto';

const TASK_CODE = 'M365_DORMANT_SYNC';
const TASK_TYPE = 'M365_DORMANT_SYNC' as const;
const ACTIVITY_REPORTS: ActivityReportName[] = [
  'EmailActivity',
  'OneDriveActivity',
  'TeamsUserActivity',
  'SharePointActivity',
];

const STUCK_THRESHOLD_MINUTES = 30;

interface EnrichedUser {
  graphUserId: string;
  userPrincipalName: string;
  displayName: string | null;
  mail: string | null;
  department: string | null;
  jobTitle: string | null;
  accountEnabled: boolean;
  accountCreatedAt: Date | null;
  hasLicense: boolean;
  licenses: Array<{ skuId: string; skuPartNumber: string; displayName: string }>;
  lastSignInDateTime: Date | null;
  lastNonInteractiveSignInDateTime: Date | null;
  lastEmailActivity: Date | null;
  lastOneDriveActivity: Date | null;
  lastTeamsActivity: Date | null;
  lastSharePointActivity: Date | null;
  lastAnyActivity: Date | null;
  daysInactive: number | null;
}

@Injectable()
export class M365SyncService implements OnModuleInit {
  private readonly logger = new Logger(M365SyncService.name);

  constructor(
    private readonly prisma: PrismaService,
    @Inject(GRAPH_CLIENT_TOKEN) private readonly graph: GraphClient,
  ) {}

  /**
   * 启动时清理 stuck running execution（进程崩溃 / 重启遗留）
   */
  @SkipAssertAccess('Startup cleanup of stuck RUNNING execution rows; system-internal, no user input or IDOR surface')
  async onModuleInit(): Promise<void> {
    try {
      const cutoff = new Date(Date.now() - STUCK_THRESHOLD_MINUTES * 60 * 1000);
      const updated = await this.prisma.automationExecution.updateMany({
        where: {
          task: { type: TASK_TYPE },
          status: 'RUNNING',
          startedAt: { lt: cutoff },
        },
        data: {
          status: 'TIMEOUT',
          completedAt: new Date(),
          error: 'SYNC_INTERRUPTED: Backend restarted while running',
        },
      });
      if (updated.count > 0) {
        this.logger.warn(`Cleaned up ${updated.count} stuck M365 sync execution(s)`);
      }
    } catch (error: any) {
      // Defensive: never let cleanup failure crash backend startup
      this.logger.warn(`Stuck-execution cleanup failed: ${error?.message ?? error}`);
    }
  }

  /**
   * 触发同步：立刻创建 RUNNING execution + 异步跑 sync，返回 executionId
   */
  async triggerSync(actorId: string, organizationId: string): Promise<{
    executionId: string;
    status: 'RUNNING';
    startedAt: Date;
  }> {
    const task = await this.ensureTask(actorId);

    const running = await this.prisma.automationExecution.findFirst({
      where: { taskId: task.id, status: 'RUNNING' },
    });
    if (running) {
      throw new SyncInProgressException(running.id);
    }

    const execution = await this.prisma.automationExecution.create({
      data: {
        taskId: task.id,
        status: 'RUNNING',
        triggerType: 'manual',
        triggeredBy: actorId,
      },
    });

    // 异步执行（不 await）
    this.runSync(execution.id, task.id, actorId, organizationId).catch((err) => {
      this.logger.error(`Sync ${execution.id} crashed: ${err?.message}`, err?.stack);
    });

    return {
      executionId: execution.id,
      status: 'RUNNING',
      startedAt: execution.startedAt,
    };
  }

  /**
   * 查询最新一次 SUCCESS execution（用于 list users）
   */
  async findLatestSuccessExecution() {
    const task = await this.findTask();
    if (!task) return null;
    return this.prisma.automationExecution.findFirst({
      where: { taskId: task.id, status: 'SUCCESS' },
      orderBy: { startedAt: 'desc' },
    });
  }

  async findLatestExecution() {
    const task = await this.findTask();
    if (!task) return null;
    return this.prisma.automationExecution.findFirst({
      where: { taskId: task.id },
      orderBy: { startedAt: 'desc' },
    });
  }

  async findHistory(limit: number, status?: string) {
    const task = await this.findTask();
    if (!task) return [];
    const where: Prisma.AutomationExecutionWhereInput = { taskId: task.id };
    if (status) where.status = status as any;
    return this.prisma.automationExecution.findMany({
      where,
      orderBy: { startedAt: 'desc' },
      take: limit,
    });
  }

  // ============ 内部 ============

  private async findTask() {
    return this.prisma.automationTask.findFirst({ where: { code: TASK_CODE } });
  }

  private async ensureTask(actorId: string) {
    let task = await this.findTask();
    if (!task) {
      task = await this.prisma.automationTask.create({
        data: {
          code: TASK_CODE,
          name: 'M365 Dormant Account Sync',
          type: TASK_TYPE,
          scheduleType: 'MANUAL',
          status: 'ACTIVE',
          createdById: actorId,
        },
      });
    }
    return task;
  }

  /**
   * 同步主流程（异步执行）
   */
  private async runSync(
    executionId: string,
    taskId: string,
    actorId: string,
    organizationId: string,
  ): Promise<void> {
    const startedAt = (await this.prisma.automationExecution.findUniqueOrThrow({
      where: { id: executionId },
    })).startedAt;

    try {
      const subscribedSkus = await this.graph.listSubscribedSkus();
      const skuMap = new Map<string, { skuId: string; skuPartNumber: string; displayName: string }>();
      for (const s of subscribedSkus) {
        skuMap.set(s.skuId, {
          skuId: s.skuId,
          skuPartNumber: s.skuPartNumber,
          displayName: resolveLicenseDisplayName(s.skuPartNumber),
        });
      }

      const users = await this.graph.listUsers();

      // 4 reports run in parallel — independent Graph endpoints
      const reportResults = await Promise.all(
        ACTIVITY_REPORTS.map(async (report) => [report, await this.graph.getActivityReport(report)] as const),
      );
      const activityMaps = Object.fromEntries(
        reportResults.map(([report, entries]) => [
          report,
          new Map(entries.map((e) => [e.userPrincipalNameLower, e.lastActivityDate])),
        ]),
      ) as Record<ActivityReportName, Map<string, Date | null>>;

      const enrichedAll: EnrichedUser[] = users.map((u) =>
        this.enrichUser(u, skuMap, activityMaps, startedAt),
      );

      let changedUserCount = 0;

      // Batched to bound transaction length and DB connection hold time
      const BATCH_SIZE = 200;
      for (let i = 0; i < enrichedAll.length; i += BATCH_SIZE) {
        const slice = enrichedAll.slice(i, i + BATCH_SIZE);
        const changedThisBatch = await this.prisma.$transaction(async (tx) => {
          let changed = 0;
          for (const enriched of slice) {
            const existing = await tx.m365User.findUnique({
              where: {
                organizationId_graphUserId: {
                  organizationId,
                  graphUserId: enriched.graphUserId,
                },
              },
            });

            if (existing) {
              const diffs = this.computeDiffs(existing, enriched);
              if (diffs.length > 0) {
                changed += 1;
                await tx.m365UserActivityChange.createMany({
                  data: diffs.map((d) => ({
                    organizationId,
                    userId: existing.id,
                    executionId,
                    field: d.field,
                    previousValue: d.previousValue,
                    currentValue: d.currentValue,
                  })),
                });
              }
              await tx.m365User.update({
                where: { id: existing.id },
                data: {
                  ...this.toPrismaData(enriched),
                  lastSeenInExecutionId: executionId,
                },
              });
            } else {
              await tx.m365User.create({
                data: {
                  organizationId,
                  createdById: actorId,
                  ...this.toPrismaData(enriched),
                  graphUserId: enriched.graphUserId,
                  firstSeenInExecutionId: executionId,
                  lastSeenInExecutionId: executionId,
                },
              });
              // First-seen users don't get change events (no prior state to diff)
            }
          }
          return changed;
        });
        changedUserCount += changedThisBatch;
      }

      // 5. 直方图聚合
      const distribution = await this.computeHistogram(organizationId);

      const result: ExecutionResultPayload = {
        totalUsers: enrichedAll.length,
        changedUsers: changedUserCount,
        inactiveDistribution: distribution,
      };

      const completedAt = new Date();
      await this.prisma.automationExecution.update({
        where: { id: executionId },
        data: {
          status: 'SUCCESS',
          completedAt,
          duration: completedAt.getTime() - startedAt.getTime(),
          result: result as any,
        },
      });
      await this.prisma.automationTask.update({
        where: { id: taskId },
        data: {
          lastRunAt: completedAt,
          lastStatus: 'SUCCESS',
          totalRuns: { increment: 1 },
          successRuns: { increment: 1 },
        },
      });
      this.logger.log(`M365 sync ${executionId} success: ${result.totalUsers} users, ${result.changedUsers} changed`);
    } catch (error: any) {
      const completedAt = new Date();
      const code = error?.response?.code ?? error?.code ?? 'GRAPH_UPSTREAM_ERROR';
      const msg = error?.response?.message ?? error?.message ?? String(error);
      await this.prisma.automationExecution.update({
        where: { id: executionId },
        data: {
          status: 'FAILED',
          completedAt,
          duration: completedAt.getTime() - startedAt.getTime(),
          error: `${code}: ${msg}`,
          logs: error?.stack ?? null,
        },
      });
      await this.prisma.automationTask.update({
        where: { id: taskId },
        data: {
          lastRunAt: completedAt,
          lastStatus: 'FAILED',
          totalRuns: { increment: 1 },
          failedRuns: { increment: 1 },
        },
      });
      this.logger.error(`M365 sync ${executionId} failed: ${code}: ${msg}`);
    }
  }

  private enrichUser(
    u: GraphUser,
    skuMap: Map<string, { skuId: string; skuPartNumber: string; displayName: string }>,
    activityMaps: Record<ActivityReportName, Map<string, Date | null>>,
    referenceTime: Date,
  ): EnrichedUser {
    const upnLower = u.userPrincipalName.toLowerCase();

    const licenses = (u.assignedLicenses ?? [])
      .map((al) => skuMap.get(al.skuId))
      .filter((x): x is { skuId: string; skuPartNumber: string; displayName: string } => !!x);

    const lastSignIn = parseDate(u.signInActivity?.lastSignInDateTime ?? null);
    const lastNonInteractive = parseDate(u.signInActivity?.lastNonInteractiveSignInDateTime ?? null);
    const lastEmail = activityMaps.EmailActivity.get(upnLower) ?? null;
    const lastOneDrive = activityMaps.OneDriveActivity.get(upnLower) ?? null;
    const lastTeams = activityMaps.TeamsUserActivity.get(upnLower) ?? null;
    const lastSharePoint = activityMaps.SharePointActivity.get(upnLower) ?? null;

    const candidates = [lastSignIn, lastNonInteractive, lastEmail, lastOneDrive, lastTeams, lastSharePoint]
      .filter((d): d is Date => d != null);
    const lastAnyActivity = candidates.length > 0
      ? new Date(Math.max(...candidates.map((d) => d.getTime())))
      : null;

    const daysInactive = lastAnyActivity
      ? Math.floor((referenceTime.getTime() - lastAnyActivity.getTime()) / (24 * 3600 * 1000))
      : null;

    return {
      graphUserId: u.id,
      userPrincipalName: u.userPrincipalName,
      displayName: u.displayName ?? null,
      mail: u.mail ?? null,
      department: u.department ?? null,
      jobTitle: u.jobTitle ?? null,
      accountEnabled: u.accountEnabled,
      accountCreatedAt: parseDate(u.createdDateTime ?? null),
      hasLicense: licenses.length > 0,
      licenses,
      lastSignInDateTime: lastSignIn,
      lastNonInteractiveSignInDateTime: lastNonInteractive,
      lastEmailActivity: lastEmail,
      lastOneDriveActivity: lastOneDrive,
      lastTeamsActivity: lastTeams,
      lastSharePointActivity: lastSharePoint,
      lastAnyActivity,
      daysInactive,
    };
  }

  private toPrismaData(e: EnrichedUser) {
    return {
      userPrincipalName: e.userPrincipalName,
      displayName: e.displayName,
      mail: e.mail,
      department: e.department,
      jobTitle: e.jobTitle,
      accountEnabled: e.accountEnabled,
      accountCreatedAt: e.accountCreatedAt,
      hasLicense: e.hasLicense,
      licenses: e.licenses as any,
      lastSignInDateTime: e.lastSignInDateTime,
      lastNonInteractiveSignInDateTime: e.lastNonInteractiveSignInDateTime,
      lastEmailActivity: e.lastEmailActivity,
      lastOneDriveActivity: e.lastOneDriveActivity,
      lastTeamsActivity: e.lastTeamsActivity,
      lastSharePointActivity: e.lastSharePointActivity,
      lastAnyActivity: e.lastAnyActivity,
      daysInactive: e.daysInactive,
    };
  }

  private computeDiffs(
    existing: any,
    enriched: EnrichedUser,
  ): Array<{ field: M365UserActivityChangeField; previousValue: string | null; currentValue: string | null }> {
    const diffs: Array<{ field: M365UserActivityChangeField; previousValue: string | null; currentValue: string | null }> = [];

    const dateFields: Array<keyof EnrichedUser & M365UserActivityChangeField> = [
      'lastSignInDateTime',
      'lastNonInteractiveSignInDateTime',
      'lastEmailActivity',
      'lastOneDriveActivity',
      'lastTeamsActivity',
      'lastSharePointActivity',
    ];

    for (const f of dateFields) {
      const prev = (existing[f] as Date | null) ?? null;
      const curr = (enriched[f] as Date | null) ?? null;
      const prevTime = prev?.getTime() ?? null;
      const currTime = curr?.getTime() ?? null;
      if (prevTime !== currTime) {
        diffs.push({
          field: f as M365UserActivityChangeField,
          previousValue: prev?.toISOString() ?? null,
          currentValue: curr?.toISOString() ?? null,
        });
      }
    }

    if (existing.accountEnabled !== enriched.accountEnabled) {
      diffs.push({
        field: 'accountEnabled' as M365UserActivityChangeField,
        previousValue: String(existing.accountEnabled),
        currentValue: String(enriched.accountEnabled),
      });
    }
    if (existing.hasLicense !== enriched.hasLicense) {
      diffs.push({
        field: 'hasLicense' as M365UserActivityChangeField,
        previousValue: String(existing.hasLicense),
        currentValue: String(enriched.hasLicense),
      });
    }

    const prevLicensesKey = JSON.stringify(
      (existing.licenses as any[]).map((l: any) => l.skuPartNumber).sort(),
    );
    const currLicensesKey = JSON.stringify(enriched.licenses.map((l) => l.skuPartNumber).sort());
    if (prevLicensesKey !== currLicensesKey) {
      diffs.push({
        field: 'licenses' as M365UserActivityChangeField,
        previousValue: prevLicensesKey,
        currentValue: currLicensesKey,
      });
    }

    return diffs;
  }

  private async computeHistogram(organizationId: string): Promise<InactiveDistribution> {
    // PostgreSQL count(*) FILTER (WHERE ...) 聚合
    const rows = await this.prisma.$queryRaw<Array<{
      never_count: bigint;
      b1: bigint;
      b2: bigint;
      b3: bigint;
      b4: bigint;
      b5: bigint;
      b6: bigint;
    }>>(Prisma.sql`
      SELECT
        count(*) FILTER (WHERE last_any_activity_at IS NULL) AS never_count,
        count(*) FILTER (WHERE days_inactive >= 0   AND days_inactive < 30)  AS b1,
        count(*) FILTER (WHERE days_inactive >= 30  AND days_inactive < 60)  AS b2,
        count(*) FILTER (WHERE days_inactive >= 60  AND days_inactive < 90)  AS b3,
        count(*) FILTER (WHERE days_inactive >= 90  AND days_inactive < 180) AS b4,
        count(*) FILTER (WHERE days_inactive >= 180 AND days_inactive < 365) AS b5,
        count(*) FILTER (WHERE days_inactive >= 365)                         AS b6
      FROM platform_ops_center.m365_users
      WHERE organization_id = ${organizationId}::uuid
    `);

    const r = rows[0];
    return {
      never: Number(r.never_count),
      buckets: [
        { from: 0, to: 30, count: Number(r.b1) },
        { from: 30, to: 60, count: Number(r.b2) },
        { from: 60, to: 90, count: Number(r.b3) },
        { from: 90, to: 180, count: Number(r.b4) },
        { from: 180, to: 365, count: Number(r.b5) },
        { from: 365, to: null, count: Number(r.b6) },
      ],
    };
  }
}

function parseDate(raw: string | null | undefined): Date | null {
  if (!raw) return null;
  const d = new Date(raw);
  return Number.isNaN(d.getTime()) ? null : d;
}
