import { Injectable, NotFoundException } from '@nestjs/common';
import { CaslAbilityFactory } from '../authorization/casl-ability.factory';
import { AuthorizationService } from '../authorization';
import { OperationLogsService } from '../operation-logs/operation-logs.service';
import { AgentToolRegistry } from './agent-tool.registry';
import { AgentToolContextFactory } from './agent-tool.types';
import type { AgentToolContext, ToolExecutionResult, ToolStatus, ToolDescriptor } from './agent-tool.types';

/** Safe tool name: alphanumeric + underscore, max 64 chars. */
const TOOL_NAME_RE = /^[a-zA-Z0-9_]+$/;
const TOOL_NAME_MAX_LEN = 64;

/** Safe user-facing messages that never leak internals. */
const SAFE_MESSAGES = {
  unknownTool: '未知工具',
  permissionDenied: '权限不足',
  invalidInput: '输入参数无效',
  executionFailed: '工具执行失败',
  notFound: '记录不存在或无权访问',
} as const;

/**
 * Executes Agent Tools with double-check authorization, input validation,
 * context trust validation, and audit logging.
 *
 * ## Security guarantees
 *
 * 1. Context trust is validated at runtime via
 *    {@link AgentToolContextFactory.assertTrusted} — forged/plain-object
 *    contexts are rejected.
 * 2. The ability is constructed fresh from the principal in the context
 *    — callers cannot pre-forge it.
 * 3. Permission is checked AGAIN at execute time (not just at list time).
 * 4. Unknown tools are rejected with a generic message, and the tool name
 *    is sanitized in audit logs.
 * 5. `rawInput` is `unknown` — null, arrays, and strings are caught before
 *    validation.
 * 6. All tool & validator exceptions are caught and mapped to safe messages.
 * 7. Audit logs never include raw input, stack traces, or internal error text.
 * 8. Audit log is awaited best-effort — failure does NOT fail the tool call.
 */
@Injectable()
export class AgentToolExecutor {
  constructor(
    private readonly registry: AgentToolRegistry,
    private readonly abilityFactory: CaslAbilityFactory,
    private readonly authz: AuthorizationService,
    private readonly opLog: OperationLogsService,
  ) {}

  /**
   * List tools available to the given context.
   *
   * Returns read-only {@link ToolDescriptor}s — never exposes
   * `execute`, `validate`, or `requiredPermission`.
   *
   * This is the ONLY public entry point for tool discovery.
   * SDK consumers MUST use this instead of direct Registry access.
   *
   * @param context — trusted context from
   *   {@link AgentToolContextFactory.fromAuthenticatedUser}.
   */
  listAvailable(context: AgentToolContext): ToolDescriptor[] {
    AgentToolContextFactory.assertTrusted(context);

    const ability = this.abilityFactory.createForUser({
      permissions: context.permissions,
      isSuperAdmin: context.isSuperAdmin,
    });

    return this.registry
      .listAvailableInternal(ability)
      .map(({ name, description, inputSchema }) => ({
        name,
        description,
        ...(inputSchema ? { inputSchema } : {}),
      }));
  }

  /**
   * Execute a tool by name.
   *
   * @param name — tool name (e.g. "search_students"). Must pass sanitization.
   * @param rawInput — raw input from the model (may be any JSON value).
   * @param context — trusted context from
   *   {@link AgentToolContextFactory.fromAuthenticatedUser}.
   */
  async execute(
    name: string,
    rawInput: unknown,
    context: AgentToolContext,
  ): Promise<ToolExecutionResult> {
    // 0. Context trust validation — must be first
    try {
      AgentToolContextFactory.assertTrusted(context);
    } catch {
      return { status: 'denied', toolName: '_denied', error: SAFE_MESSAGES.permissionDenied };
    }

    // 1. Sanitize tool name — model-controlled input
    const safeName = this.sanitizeName(name);

    const tool = this.registry.getForExecution(name);
    if (!tool) {
      return this.auditAndReturn(
        safeName,
        'denied',
        undefined,
        SAFE_MESSAGES.unknownTool,
        context,
      );
    }

    // 2. Build ability from principal fields — never trust a pre-built one
    const ability = this.abilityFactory.createForUser({
      permissions: context.permissions,
      isSuperAdmin: context.isSuperAdmin,
    });

    // 3. Double-check authorization at execute time
    if (!this.authz.canPermission(ability, tool.requiredPermission)) {
      return this.auditAndReturn(
        safeName,
        'denied',
        undefined,
        SAFE_MESSAGES.permissionDenied,
        context,
      );
    }

    // 4. Guard: rawInput must be a plain object
    if (rawInput === null || Array.isArray(rawInput) || typeof rawInput !== 'object') {
      return this.auditAndReturn(
        safeName,
        'failed',
        undefined,
        SAFE_MESSAGES.invalidInput,
        context,
      );
    }

    // 5. Validate and parse input — validator exceptions are caught
    let parsed: { ok: true; value: unknown } | { ok: false };
    try {
      parsed = tool.validate(rawInput as Record<string, unknown>);
    } catch {
      return this.auditAndReturn(
        safeName,
        'failed',
        undefined,
        SAFE_MESSAGES.invalidInput,
        context,
      );
    }
    if (!parsed.ok) {
      return this.auditAndReturn(
        safeName,
        'failed',
        undefined,
        SAFE_MESSAGES.invalidInput,
        context,
      );
    }

    // 6. Execute
    try {
      const result = await tool.execute(parsed.value, context);
      return this.auditAndReturn(safeName, 'success', result, undefined, context);
    } catch (err: unknown) {
      // NotFoundException → not_found with safe message
      if (err instanceof NotFoundException) {
        return this.auditAndReturn(
          safeName,
          'not_found',
          undefined,
          SAFE_MESSAGES.notFound,
          context,
        );
      }
      // All other errors → generic failed message
      return this.auditAndReturn(
        safeName,
        'failed',
        undefined,
        SAFE_MESSAGES.executionFailed,
        context,
      );
    }
  }

  /**
   * Sanitize a tool name from model input.
   *
   * Only allows `[a-zA-Z0-9_]`, max {@link TOOL_NAME_MAX_LEN} chars.
   * Returns the sanitized name or a safe fallback.
   */
  private sanitizeName(name: string): string {
    if (typeof name !== 'string') return '_invalid';
    const trimmed = name.slice(0, TOOL_NAME_MAX_LEN);
    if (TOOL_NAME_RE.test(trimmed)) return trimmed;
    // Replace unsafe chars with underscore
    return trimmed.replace(/[^a-zA-Z0-9_]/g, '_').slice(0, TOOL_NAME_MAX_LEN);
  }

  /**
   * Build result + best-effort awaited audit log.
   * Audit write failure is caught and never propagated — it must not
   * turn a successful data read into a failure.
   */
  private async auditAndReturn(
    toolName: string,
    status: ToolStatus,
    result: unknown,
    error: string | undefined,
    context: AgentToolContext,
  ): Promise<ToolExecutionResult> {
    // Await audit (best-effort — failure is silently swallowed)
    try {
      await this.opLog.log({
        userId: context.userId,
        username: context.username,
        module: 'AI Agent Tool',
        action: `${toolName} [${status}]`,
        detail: this.buildAuditDetail(status),
        status,
      });
    } catch {
      // Swallow — audit failure must not break the tool call
    }

    return { status, toolName, result, error };
  }

  /**
   * Build a safe audit detail string.
   * NEVER includes raw input, exception messages, phone numbers, or other PII.
   * Only writes safe category labels.
   */
  private buildAuditDetail(status: ToolStatus): string {
    switch (status) {
      case 'success':
        return '执行成功';
      case 'denied':
        return '拒绝访问';
      case 'not_found':
        return '记录不存在或无权访问';
      default:
        return '执行失败';
    }
  }
}
