import { createHash, randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import { eq, users as usersTable, type Db } from '@mosaicstack/db';
import type { CommandDef, SlashCommandPayload } from '@mosaicstack/types';
import { DB } from '../database/database.module.js';
import { COMMANDS_REDIS } from './commands.tokens.js';

export type CommandRole = 'admin' | 'member' | 'viewer';

export interface CommandApproval {
  approvalId: string;
  actionDigest: string;
  actorId: string;
  command: string;
  expiresAt: string;
}

/** Exact immutable binding for a privileged runtime termination. */
export interface RuntimeTerminationApprovalAction {
  providerId: string;
  sessionId: string;
  actorId: string;
  tenantId: string;
  channelId: string;
  correlationId: string;
  /** Provisioned roster identity; isolates approvals between interaction agents. */
  agentName: string;
}

export interface RuntimeTerminationApproval extends RuntimeTerminationApprovalAction {
  approvalId: string;
  actionDigest: string;
  expiresAt: string;
}

export interface CommandAuthorizationResult {
  allowed: boolean;
  reason?: string;
}

@Injectable()
export class CommandAuthorizationService {
  constructor(
    @Inject(DB) private readonly db: Db,
    @Inject(COMMANDS_REDIS)
    private readonly redis: {
      get(key: string): Promise<string | null>;
      set(key: string, value: string, ...args: string[]): Promise<unknown>;
      del(key: string): Promise<number>;
    },
  ) {}

  async authorize(
    command: CommandDef,
    payload: SlashCommandPayload,
    actorId: string,
    approvalId?: string,
  ): Promise<CommandAuthorizationResult> {
    const role = await this.resolveRole(actorId);
    if (!role || !this.hasScope(role, command.scope)) {
      return { allowed: false, reason: 'not authorized for this command scope' };
    }
    if (command.scope !== 'admin') return { allowed: true };
    if (!approvalId) return { allowed: false, reason: 'durable approval is required' };
    const actionDigest = this.actionDigest(command.name, payload);
    const approved = await this.consumeApproval(approvalId, actorId, actionDigest);
    return approved
      ? { allowed: true }
      : {
          allowed: false,
          reason: 'approval is invalid, expired, replayed, or does not match this action',
        };
  }

  async createApproval(
    command: CommandDef,
    payload: SlashCommandPayload,
    actorId: string,
  ): Promise<CommandApproval | null> {
    const role = await this.resolveRole(actorId);
    if (!role || command.scope !== 'admin' || !this.hasScope(role, command.scope)) return null;

    const approvalId = randomUUID();
    const expiresAt = new Date(Date.now() + 5 * 60_000).toISOString();
    const approval: CommandApproval = {
      approvalId,
      actionDigest: this.actionDigest(command.name, payload),
      actorId,
      command: command.name,
      expiresAt,
    };
    await this.redis.set(this.key(approvalId), JSON.stringify(approval), 'EX', '300');
    return approval;
  }

  /**
   * Uses the same `interaction:command-approval:*` store and one-time deletion rule as
   * command approvals. This deliberately avoids a parallel approval database.
   */
  async createRuntimeTerminationApproval(
    action: RuntimeTerminationApprovalAction,
  ): Promise<RuntimeTerminationApproval | null> {
    if (!this.hasRuntimeTerminationAction(action)) return null;
    const role = await this.resolveRole(action.actorId);
    if (role !== 'admin') return null;

    const approval: RuntimeTerminationApproval = {
      approvalId: randomUUID(),
      actionDigest: this.runtimeActionDigest(action),
      ...action,
      expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),
    };
    await this.redis.set(
      this.runtimeKey(action.agentName, approval.approvalId),
      JSON.stringify(approval),
      'EX',
      '300',
    );
    return approval;
  }

  async consumeRuntimeTerminationApproval(
    approvalId: string,
    action: RuntimeTerminationApprovalAction,
  ): Promise<boolean> {
    const encoded = await this.redis.get(this.runtimeKey(action.agentName, approvalId));
    if (!encoded) return false;
    let approval: unknown;
    try {
      approval = JSON.parse(encoded);
    } catch {
      return false;
    }
    if (
      !this.isRuntimeTerminationApproval(approval) ||
      approval.actionDigest !== this.runtimeActionDigest(action) ||
      approval.actorId !== action.actorId ||
      approval.tenantId !== action.tenantId ||
      !this.isUnexpired(approval.expiresAt)
    ) {
      return false;
    }
    if ((await this.resolveRole(approval.actorId)) !== 'admin') return false;
    return (await this.redis.del(this.runtimeKey(action.agentName, approvalId))) === 1;
  }

  private async resolveRole(actorId: string): Promise<CommandRole | null> {
    const [user] = await this.db
      .select({ role: usersTable.role })
      .from(usersTable)
      .where(eq(usersTable.id, actorId))
      .limit(1);
    const role = user?.role;
    return role === 'admin' || role === 'member' || role === 'viewer' ? role : null;
  }

  private hasScope(role: CommandRole, scope: CommandDef['scope']): boolean {
    if (role === 'admin') return true;
    return role === 'member' && (scope === 'core' || scope === 'agent');
  }

  private async consumeApproval(
    approvalId: string,
    actorId: string,
    actionDigest: string,
  ): Promise<boolean> {
    const key = this.key(approvalId);
    const encoded = await this.redis.get(key);
    if (!encoded) return false;
    let parsed: unknown;
    try {
      parsed = JSON.parse(encoded);
    } catch {
      return false;
    }
    if (
      !this.isCommandApproval(parsed) ||
      parsed.actorId !== actorId ||
      parsed.actionDigest !== actionDigest ||
      !this.isUnexpired(parsed.expiresAt)
    )
      return false;
    return (await this.redis.del(key)) === 1;
  }

  private actionDigest(command: string, payload: SlashCommandPayload): string {
    return createHash('sha256')
      .update(
        JSON.stringify({
          command,
          args: payload.args?.trim() ?? '',
          conversationId: payload.conversationId,
        }),
      )
      .digest('hex');
  }

  private hasRuntimeTerminationAction(action: RuntimeTerminationApprovalAction): boolean {
    return [
      action.providerId,
      action.sessionId,
      action.actorId,
      action.tenantId,
      action.channelId,
      action.correlationId,
      action.agentName,
    ].every((value: string): boolean => value.trim().length > 0);
  }

  private runtimeActionDigest(action: RuntimeTerminationApprovalAction): string {
    return createHash('sha256')
      .update(
        JSON.stringify({
          providerId: action.providerId,
          sessionId: action.sessionId,
          actorId: action.actorId,
          tenantId: action.tenantId,
          channelId: action.channelId,
          correlationId: action.correlationId,
          agentName: action.agentName,
        }),
      )
      .digest('hex');
  }

  private isUnexpired(expiresAt: unknown): expiresAt is string {
    if (typeof expiresAt !== 'string') return false;
    const expiresAtMs = Date.parse(expiresAt);
    return Number.isFinite(expiresAtMs) && expiresAtMs > Date.now();
  }

  private isCommandApproval(value: unknown): value is CommandApproval {
    return (
      typeof value === 'object' &&
      value !== null &&
      'approvalId' in value &&
      'actionDigest' in value &&
      'actorId' in value &&
      'expiresAt' in value &&
      'command' in value
    );
  }

  private isRuntimeTerminationApproval(value: unknown): value is RuntimeTerminationApproval {
    return (
      typeof value === 'object' &&
      value !== null &&
      'approvalId' in value &&
      'actionDigest' in value &&
      'actorId' in value &&
      'tenantId' in value &&
      'providerId' in value &&
      'sessionId' in value &&
      'channelId' in value &&
      'correlationId' in value &&
      'agentName' in value &&
      'expiresAt' in value
    );
  }

  private key(approvalId: string): string {
    return `interaction:command-approval:${approvalId}`;
  }

  private runtimeKey(agentName: string, approvalId: string): string {
    return `agent:${encodeURIComponent(agentName)}:command-approval:${approvalId}`;
  }
}
