import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '@core/database/prisma/prisma.service';

/**
 * internal_app_events 写入 + 查询服务。
 *
 * 业务约束（详见 docs/modules/internal-app-platform/06-data-model.md §2.6）：
 * - emit 是 best-effort：写失败不抛、不回滚业务操作，仅 logger.warn
 * - payload 禁写 env value / token 明文（仅记 key/prefix）
 * - 与 `audit-system` 分工见 §7.1：合规事件双写两边，运营事件只走本表
 */

export type ActorRole = 'OWNER' | 'ADMIN' | 'SYSTEM';
export type EventOutcome = 'OK' | 'FAIL';

/**
 * 事件类型枚举（仅本模块业务事件；扩展不需 schema migration，但应同步更新本枚举 + 06-data-model §2.6 表）
 */
export const EventType = {
  // token (5)
  TOKEN_ISSUED: 'token.issued',
  TOKEN_REGENERATED: 'token.regenerated',
  TOKEN_REVOKED: 'token.revoked',
  TOKEN_EXPIRED: 'token.expired',
  TOKEN_DISABLED: 'token.disabled',
  // app lifecycle (4)
  APP_CREATED: 'app.created',
  APP_DESTROYED: 'app.destroyed',
  APP_RECOVERED: 'app.recovered',
  APP_PURGED: 'app.purged',
  // deploy (3)
  APP_DEPLOY_STARTED: 'app.deploy_started',
  APP_DEPLOY_SUCCEEDED: 'app.deploy_succeeded',
  APP_DEPLOY_FAILED: 'app.deploy_failed',
  // env (2)
  APP_ENV_SET: 'app.env_set',
  APP_ENV_UNSET: 'app.env_unset',
  // admin (2)
  APP_DISABLED_BY_ADMIN: 'app.disabled_by_admin',
  APP_FORCE_DESTROYED_BY_ADMIN: 'app.force_destroyed_by_admin',
} as const;

export type EventTypeValue = (typeof EventType)[keyof typeof EventType];

export interface EmitEventParams {
  eventType: EventTypeValue | string;
  actorRole: ActorRole;
  organizationId: string;
  appId?: string | null;
  employeeSlug?: string | null;
  actorId?: string | null;
  outcome?: EventOutcome;
  errorCode?: string | null;
  durationMs?: number | null;
  payload?: Record<string, unknown>;
  requestId?: string | null;
  ipAddr?: string | null;
  userAgent?: string | null;
}

export interface ListEventsFilter {
  organizationId: string;
  appId?: string;
  employeeSlug?: string;
  actorRole?: ActorRole;
  eventTypes?: string[];
  outcome?: EventOutcome;
  from?: Date;
  to?: Date;
  page?: number;
  pageSize?: number;
}

export interface EventListItem {
  id: string;
  appId: string | null;
  appSlug: string | null;
  employeeSlug: string | null;
  actorId: string | null;
  actorEmail: string | null;
  actorRole: ActorRole;
  eventType: string;
  outcome: EventOutcome;
  errorCode: string | null;
  durationMs: number | null;
  payload: Record<string, unknown>;
  ipAddr: string | null;
  createdAt: Date;
}

const MAX_PAYLOAD_BYTES = 8 * 1024; // 8 KB 上限，超出截断
const DEFAULT_PAGE_SIZE = 50;
const MAX_PAGE_SIZE = 200;

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

  constructor(private readonly prisma: PrismaService) {}

  /**
   * 写一条事件。best-effort：失败仅 warn，不抛、不回滚调用方。
   */
  async emit(params: EmitEventParams): Promise<void> {
    try {
      const payload = this.sanitizePayload(params.payload);
      await this.prisma.internalAppEvent.create({
        data: {
          appId: params.appId ?? null,
          employeeSlug: params.employeeSlug ?? null,
          actorId: params.actorId ?? null,
          actorRole: params.actorRole,
          eventType: params.eventType,
          outcome: params.outcome ?? 'OK',
          errorCode: params.errorCode ?? null,
          durationMs: params.durationMs ?? null,
          payload: payload as Prisma.InputJsonValue,
          requestId: params.requestId ?? null,
          ipAddr: params.ipAddr ?? null,
          userAgent: params.userAgent ?? null,
          organizationId: params.organizationId,
        },
      });
    } catch (err) {
      this.logger.warn(
        `emit event failed (eventType=${params.eventType}, appId=${params.appId ?? 'null'}): ${(err as Error).message}`,
      );
    }
  }

  /**
   * 查询事件流。admin / me 两个端点共用，由 controller 决定 filter（me 必须强制 employeeSlug）。
   */
  async list(filter: ListEventsFilter): Promise<{
    items: EventListItem[];
    total: number;
    page: number;
    pageSize: number;
  }> {
    const page = Math.max(1, filter.page ?? 1);
    const pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, filter.pageSize ?? DEFAULT_PAGE_SIZE));

    const where: Prisma.InternalAppEventWhereInput = {
      organizationId: filter.organizationId,
      ...(filter.appId && { appId: filter.appId }),
      ...(filter.employeeSlug && { employeeSlug: filter.employeeSlug }),
      ...(filter.actorRole && { actorRole: filter.actorRole }),
      ...(filter.eventTypes &&
        filter.eventTypes.length > 0 && { eventType: { in: filter.eventTypes } }),
      ...(filter.outcome && { outcome: filter.outcome }),
      ...(filter.from || filter.to
        ? {
            createdAt: {
              ...(filter.from && { gte: filter.from }),
              ...(filter.to && { lte: filter.to }),
            },
          }
        : {}),
    };

    const [rows, total] = await this.prisma.$transaction([
      this.prisma.internalAppEvent.findMany({
        where,
        orderBy: { createdAt: 'desc' },
        skip: (page - 1) * pageSize,
        take: pageSize,
        include: {
          app: { select: { appSlug: true } },
          actor: { select: { email: true } },
        },
      }),
      this.prisma.internalAppEvent.count({ where }),
    ]);

    const items: EventListItem[] = rows.map((r) => ({
      id: r.id,
      appId: r.appId,
      appSlug: r.app?.appSlug ?? null,
      employeeSlug: r.employeeSlug,
      actorId: r.actorId,
      actorEmail: r.actor?.email ?? null,
      actorRole: r.actorRole as ActorRole,
      eventType: r.eventType,
      outcome: r.outcome as EventOutcome,
      errorCode: r.errorCode,
      durationMs: r.durationMs,
      payload: (r.payload ?? {}) as Record<string, unknown>,
      ipAddr: r.ipAddr,
      createdAt: r.createdAt,
    }));

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

  /**
   * 截断超大 payload（防止意外把 buildLog 全文塞进事件流）。
   */
  private sanitizePayload(payload?: Record<string, unknown>): Record<string, unknown> {
    if (!payload) return {};
    const serialized = JSON.stringify(payload);
    if (serialized.length <= MAX_PAYLOAD_BYTES) return payload;
    return {
      ...payload,
      _truncated: true,
      _originalBytes: serialized.length,
    };
  }
}
