import { Injectable, Logger } from '@nestjs/common';
import { createHash, randomBytes } from 'crypto';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { SkipAssertAccess } from '@common/decorators/skip-assert-access.decorator';
import { InternalAppSlugService } from './slug.service';
import { InternalAppEventsService, EventType } from './events.service';

const TOKEN_TTL_DAYS = 90;
const TOKEN_WARNING_DAYS = 7;
// 员工 Claude Code 用的 MCP server URL。默认走 FF AI Workspace 公网域名（Phase 1）；
// Phase 0 测试服通过 INTERNAL_APP_MCP_PUBLIC_URL env 覆盖到测试服公网 IP。
const MCP_ENDPOINT_URL =
  process.env.INTERNAL_APP_MCP_PUBLIC_URL ??
  'https://ffworkspace.faradayfuture.com/api/v1/internal-apps/mcp';

export interface IssuedToken {
  tokenPlaintext: string;
  mcpEndpoint: string;
  mcpAddCommand: string;
  expiresAt: Date;
  employeeSlug: string;
}

/**
 * employee token 颁发 / 撤销 / 校验服务
 *
 * 核心不变量（PRD §核心业务约束 + 07-api §8.2）：
 * - 明文 token 永不入库，仅 SHA256 哈希存表
 * - 同一 employeeSlug 同时只能有一个 ACTIVE token（DB partial unique index 强制）
 * - 重新生成 = 先 REVOKED 旧的（reason=rotated）再 INSERT 新的，事务原子
 *
 * 详见 docs/modules/internal-app-platform/06-data-model.md §2.5
 */
@Injectable()
export class InternalAppTokenService {
  private readonly logger = new Logger(InternalAppTokenService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly slugSvc: InternalAppSlugService,
    private readonly eventsSvc: InternalAppEventsService,
  ) {}

  /**
   * 颁发 token（FF AI Workspace Web 调）
   * - 首次接入员工 → 同时创建 employee_slug_bindings
   * - 已有 ACTIVE token → 同事务内 revoke 旧的，颁发新的
   */
  async issue(params: {
    userId: string;
    mailNickname: string;
    organizationId: string;
  }): Promise<IssuedToken> {
    const { userId, mailNickname, organizationId } = params;

    // 拟用 slug（用于首次创建 binding）
    const computedSlug = this.slugSvc.normalizeEmployeeSlug(mailNickname);

    const tokenPlaintext = this.generateOpaqueToken();
    const tokenHash = this.hashToken(tokenPlaintext);
    const prefix = tokenPlaintext.slice(0, 8);
    const expiresAt = new Date(Date.now() + TOKEN_TTL_DAYS * 24 * 3600 * 1000);

    // 终身冻结的真实 slug 在 binding 行里——upsert update:{} 时 computedSlug
    // 不会覆盖现有值。从 upsert 返回值取真实 slug，避免 FK 漂移。
    let employeeSlug = computedSlug;
    let replacedActiveCount = 0;
    let tokenOrgId = organizationId;

    await this.prisma.$transaction(async (tx) => {
      const binding = await tx.employeeSlugBinding.upsert({
        where: { userId },
        create: {
          userId,
          employeeSlug: computedSlug,
          sourceMailNickname: mailNickname,
          organizationId,
          createdById: userId,
        },
        update: {}, // slug 终身冻结，已有 binding 一律不改
        select: { employeeSlug: true, organizationId: true },
      });
      employeeSlug = binding.employeeSlug;
      tokenOrgId = binding.organizationId;

      const revoked = await tx.internalAppEmployeeToken.updateMany({
        where: { employeeSlug, status: 'ACTIVE' },
        data: {
          status: 'REVOKED',
          revokedAt: new Date(),
          revokedReason: 'rotated',
        },
      });
      replacedActiveCount = revoked.count;

      await tx.internalAppEmployeeToken.create({
        data: {
          employeeSlug,
          tokenHash,
          prefix,
          expiresAt,
          organizationId: binding.organizationId,
          createdById: userId,
        },
      });
    });

    this.logger.log(`token issued prefix=${prefix} employeeSlug=${employeeSlug}`);

    await this.eventsSvc.emit({
      eventType: replacedActiveCount > 0 ? EventType.TOKEN_REGENERATED : EventType.TOKEN_ISSUED,
      actorRole: 'OWNER',
      organizationId: tokenOrgId,
      employeeSlug,
      actorId: userId,
      payload: {
        prefix,
        expiresAt: expiresAt.toISOString(),
        ...(replacedActiveCount > 0 && { replacedCount: replacedActiveCount }),
      },
    });

    return {
      tokenPlaintext,
      mcpEndpoint: MCP_ENDPOINT_URL,
      // Claude Code CLI 加 HTTP MCP 的正确入口：shell 子命令 `claude mcp add --transport http ...`。
      // 不是 `/mcp add`（不存在的 slash 命令），也不能省略 --transport http（默认 stdio 会把 URL 当本地命令）。
      mcpAddCommand: `claude mcp add --transport http ffoa-apps ${MCP_ENDPOINT_URL} --header "Authorization: Bearer ${tokenPlaintext}"`,
      expiresAt,
      employeeSlug,
    };
  }

  /**
   * 校验 bearer token（MCP 调用入口）
   * 返回 { employeeSlug, warningIfExpiring } 或抛错
   */
  async verify(tokenPlaintext: string): Promise<{
    employeeSlug: string;
    warning?: string;
  }> {
    const tokenHash = this.hashToken(tokenPlaintext);
    const record = await this.prisma.internalAppEmployeeToken.findUnique({
      where: { tokenHash },
    });

    if (!record) {
      throw new Error('invalid_token');
    }
    if (record.status === 'REVOKED') throw new Error('revoked_token');
    if (record.status === 'DISABLED') throw new Error('disabled_token');
    if (record.expiresAt < new Date() || record.status === 'EXPIRED') {
      throw new Error('expired_token');
    }

    const daysLeft = Math.ceil(
      (record.expiresAt.getTime() - Date.now()) / (24 * 3600 * 1000),
    );
    const warning =
      daysLeft <= TOKEN_WARNING_DAYS
        ? `token 还有 ${daysLeft} 天过期，去 https://ffworkspace.faradayfuture.com/internal-apps 续期`
        : undefined;

    return { employeeSlug: record.employeeSlug, warning };
  }

  /**
   * 撤销当前员工的 ACTIVE token（员工自助 / 7-api §4.1.2）
   * 强确认由 controller 层校验 confirmText === 'REVOKE'
   */
  @SkipAssertAccess('updateMany 已用 employeeSlug 过滤；caller 从 session 解出，非 user-supplied；员工只能撤销自己的 token，无 IDOR 风险')
  async revokeCurrent(employeeSlug: string): Promise<{ revokedAt: Date | null }> {
    const result = await this.prisma.internalAppEmployeeToken.updateMany({
      where: { employeeSlug, status: 'ACTIVE' },
      data: {
        status: 'REVOKED',
        revokedAt: new Date(),
        revokedReason: 'self',
      },
    });

    if (result.count === 0) {
      // 无 ACTIVE token 可撤销 — 静默成功（幂等）
      this.logger.warn(`revokeCurrent: no ACTIVE token for ${employeeSlug}`);
      return { revokedAt: null };
    }

    const now = new Date();
    this.logger.log(`token revoked employeeSlug=${employeeSlug} count=${result.count}`);

    const binding = await this.prisma.employeeSlugBinding.findUnique({
      where: { employeeSlug },
      select: { organizationId: true, userId: true },
    });
    if (binding) {
      await this.eventsSvc.emit({
        eventType: EventType.TOKEN_REVOKED,
        actorRole: 'OWNER',
        organizationId: binding.organizationId,
        employeeSlug,
        actorId: binding.userId,
        payload: { reason: 'self', count: result.count },
      });
    }

    return { revokedAt: now };
  }

  /**
   * 查我的 token 状态（FF AI Workspace 接入页用，07-api §4.1.3）
   * 返回最近一个 token 的状态摘要（不暴露 hash / 明文）
   */
  async getMyTokenStatus(employeeSlug: string) {
    const token = await this.prisma.internalAppEmployeeToken.findFirst({
      where: { employeeSlug },
      orderBy: { issuedAt: 'desc' },
    });

    if (!token) {
      return {
        hasToken: false,
        status: null,
        prefix: null,
        issuedAt: null,
        expiresAt: null,
        expiringInDays: null,
        lastUsedAt: null,
      };
    }

    const expiringInDays =
      token.status === 'ACTIVE'
        ? Math.ceil((token.expiresAt.getTime() - Date.now()) / (24 * 3600 * 1000))
        : null;

    return {
      hasToken: true,
      status: token.status,
      prefix: token.prefix,
      issuedAt: token.issuedAt.toISOString(),
      expiresAt: token.expiresAt.toISOString(),
      expiringInDays,
      lastUsedAt: token.lastUsedAt?.toISOString() ?? null,
    };
  }

  /** 生成 opaque token：'ffoa_' + 32 字符 base32（PRD §07-api §8.2） */
  private generateOpaqueToken(): string {
    const random = randomBytes(20)
      .toString('base64')
      .replace(/[^A-Za-z0-9]/g, '')
      .slice(0, 32);
    return `ffoa_${random}`;
  }

  private hashToken(token: string): string {
    return createHash('sha256').update(token).digest('hex');
  }
}
