// src/core/security/TopicClaimRegistry.ts

import type { CapabilityRight } from './ICapabilityToken';
import type {
  ITopicClaim,
  IClaimResult,
  IClaimOptions,
  IClaimQuery,
} from './ITopicClaim';
import type { ITopicClaimRegistry, ITopicClaimRegistryConfig } from './ITopicClaimRegistry';

/**
 * Default capabilities granted when claiming a prefix.
 *
 * Mark Miller: "Least privilege by default. Grant only what's needed."
 */
const DEFAULT_CAPABILITIES: CapabilityRight[] = ['read', 'write', 'invoke', 'emit'];

// ═══════════════════════════════════════════════════════════════════════════
// PREFIX VALIDATION (Mark Miller - Capability Security)
// ═══════════════════════════════════════════════════════════════════════════

/**
 * Validation result for prefix checks.
 */
interface IPrefixValidationResult {
  valid: boolean;
  error?: string;
}

/**
 * Validate a topic prefix to prevent injection attacks.
 *
 * Mark Miller (Capability Security, E Language):
 *   "Input validation is the first line of defense. Never trust external data."
 *
 * Butler Lampson (Turing Award, Capability Systems):
 *   "Namespace segments must be validated to prevent path traversal and injection."
 *
 * Validates against:
 * - Empty or non-string values
 * - Path traversal (..)
 * - Wildcard injection (# and +)
 * - Null bytes
 * - Leading/trailing dots
 *
 * NOTE: Slashes (/) are NOT blocked because owner mount paths use them
 * (e.g., 'flowpad/tree'). The prefix itself uses DOT (.) for hierarchy.
 *
 * @param prefix - The prefix to validate
 * @returns Validation result with valid flag and optional error
 */
function validatePrefix(prefix: string): IPrefixValidationResult {
  // Check for empty or non-string
  if (!prefix || typeof prefix !== 'string') {
    return { valid: false, error: 'prefix must be a non-empty string' };
  }

  // Check for path traversal
  if (prefix.includes('..')) {
    return { valid: false, error: 'prefix cannot contain path traversal (..)' };
  }

  // Check for wildcard injection
  if (prefix.includes('#')) {
    return { valid: false, error: 'prefix cannot contain multi-level wildcard (#)' };
  }

  if (prefix.includes('+')) {
    return { valid: false, error: 'prefix cannot contain single-level wildcard (+)' };
  }

  // Check for null bytes
  if (prefix.includes('\0')) {
    return { valid: false, error: 'prefix cannot contain null bytes' };
  }

  // Check for leading dots (would create empty segment)
  if (prefix.startsWith('.')) {
    return { valid: false, error: 'prefix cannot start with a dot' };
  }

  // Check for empty segments (consecutive dots)
  if (prefix.includes('..')) {
    return { valid: false, error: 'prefix cannot contain empty segments (..)' };
  }

  return { valid: true };
}

/**
 * Validate an owner mount path.
 *
 * Owner paths use slashes (/) for hierarchy (e.g., 'flowpad/tree').
 * We validate for injection attacks but allow the slash separator.
 *
 * @param owner - The owner mount path to validate
 * @returns Validation result
 */
function validateOwner(owner: string): IPrefixValidationResult {
  if (!owner || typeof owner !== 'string') {
    return { valid: false, error: 'owner must be a non-empty string' };
  }

  // Check for wildcard injection
  if (owner.includes('#') || owner.includes('+')) {
    return { valid: false, error: 'owner cannot contain wildcards (# or +)' };
  }

  // Check for null bytes
  if (owner.includes('\0')) {
    return { valid: false, error: 'owner cannot contain null bytes' };
  }

  return { valid: true };
}

/**
 * TopicClaimRegistry - Implementation of topic prefix claiming.
 *
 * Vision 27 §1.5 (Separator Semantics):
 * - DOT (.) separates supervision hierarchy levels
 * - 'flowpad.tree' is a child of 'flowpad'
 * - Prefix matching uses startsWith for efficiency
 *
 * Joe Armstrong (Erlang, "Programming Erlang"):
 *   "The registry is the phone book. Without it, you can't call anyone."
 *
 * Implementation notes:
 * - Uses Map<prefix, claim> for O(1) exact lookups
 * - Prefix ancestor search is O(n) where n = depth of hierarchy
 * - Suitable for thousands of components; for millions, use a trie
 *
 * Thread safety: NOT THREAD-SAFE.
 * In JavaScript's single-threaded event loop, this is fine.
 * If used in Web Workers, each worker needs its own registry.
 */
export class TopicClaimRegistry implements ITopicClaimRegistry {
  /**
   * Map of prefix -> claim for efficient exact lookups.
   */
  private readonly claims = new Map<string, ITopicClaim>();

  /**
   * Index of owner -> prefixes for efficient getClaimsByOwner.
   */
  private readonly ownerIndex = new Map<string, Set<string>>();

  /**
   * Index of delegatedBy -> prefixes for efficient cascade revocation.
   */
  private readonly delegationIndex = new Map<string, Set<string>>();

  /**
   * Configuration options for this registry.
   */
  private readonly config: Required<ITopicClaimRegistryConfig>;

  /**
   * Create a new TopicClaimRegistry.
   *
   * @param config - Optional configuration
   */
  constructor(config: ITopicClaimRegistryConfig = {}) {
    this.config = {
      strictMode: config.strictMode ?? false,
    };
  }

  /**
   * Check if strict mode is enabled.
   *
   * Butler Lampson (Turing Award, Capability Systems):
   *   "Explicit authority is safer than implicit. Strict mode enforces this."
   *
   * @returns true if strict mode is enabled
   */
  isStrictMode(): boolean {
    return this.config.strictMode;
  }

  /**
   * Claim authority over a topic prefix.
   *
   * Rules:
   * 1. Cannot claim if exact prefix already claimed (unless same owner extending)
   * 2. Cannot claim if would conflict with existing exclusive claim
   * 3. Delegated claims require parent to own an ancestor prefix
   */
  claim(
    prefix: string,
    owner: string,
    options: IClaimOptions & { delegatedBy?: string } = {}
  ): IClaimResult {
    const {
      capabilities = DEFAULT_CAPABILITIES,
      exclusive = true,
      allowExtension = true,
      delegatedBy,
    } = options;

    // Validate prefix (Mark Miller - input validation is first line of defense)
    const prefixValidation = validatePrefix(prefix);
    if (!prefixValidation.valid) {
      return {
        success: false,
        error: `invalid_prefix: ${prefixValidation.error}`,
      };
    }

    // Validate owner
    const ownerValidation = validateOwner(owner);
    if (!ownerValidation.valid) {
      return {
        success: false,
        error: `invalid_owner: ${ownerValidation.error}`,
      };
    }

    // Validate delegatedBy if provided
    if (delegatedBy) {
      const delegatorValidation = validateOwner(delegatedBy);
      if (!delegatorValidation.valid) {
        return {
          success: false,
          error: `invalid_delegator: ${delegatorValidation.error}`,
        };
      }
    }

    // Normalize prefix (remove trailing dots)
    const normalizedPrefix = this.normalizePrefix(prefix);

    // Check for existing exact claim
    const existingClaim = this.claims.get(normalizedPrefix);
    if (existingClaim && !existingClaim.revoked) {
      // Same owner extending their claim? Allow it.
      if (existingClaim.owner === owner && allowExtension) {
        // Update capabilities (union of existing and new)
        const mergedCapabilities = this.mergeCapabilities(
          existingClaim.capabilities,
          capabilities
        );
        const updatedClaim: ITopicClaim = {
          ...existingClaim,
          capabilities: mergedCapabilities,
        };
        this.claims.set(normalizedPrefix, updatedClaim);
        return { success: true, claim: updatedClaim };
      }

      // Different owner - conflict
      return {
        success: false,
        error: 'prefix_already_claimed',
        conflictingClaim: existingClaim,
      };
    }

    // Check for delegation authorization
    if (delegatedBy) {
      const parentAuthority = this.findAuthoritativePrefix(normalizedPrefix, delegatedBy);
      if (!parentAuthority) {
        return {
          success: false,
          error: 'parent_prefix_not_owned',
        };
      }
    }

    // Check for conflicting exclusive claims (either direction)
    // Pass delegatedBy so we skip conflict with the delegating parent
    const conflict = this.findConflictingClaim(normalizedPrefix, owner, exclusive, delegatedBy);
    if (conflict) {
      return {
        success: false,
        error: 'prefix_conflict',
        conflictingClaim: conflict,
      };
    }

    // Create the claim
    const claim: ITopicClaim = {
      prefix: normalizedPrefix,
      owner,
      delegatedBy,
      claimedAt: Date.now(),
      capabilities,
      exclusive,
      revoked: false,
    };

    // Store the claim
    this.claims.set(normalizedPrefix, claim);

    // Update owner index
    if (!this.ownerIndex.has(owner)) {
      this.ownerIndex.set(owner, new Set());
    }
    this.ownerIndex.get(owner)!.add(normalizedPrefix);

    // Update delegation index
    if (delegatedBy) {
      if (!this.delegationIndex.has(delegatedBy)) {
        this.delegationIndex.set(delegatedBy, new Set());
      }
      this.delegationIndex.get(delegatedBy)!.add(normalizedPrefix);
    }

    return { success: true, claim };
  }

  /**
   * Release a topic claim.
   */
  release(prefix: string, owner: string, reason = 'explicit_release'): boolean {
    const normalizedPrefix = this.normalizePrefix(prefix);
    const claim = this.claims.get(normalizedPrefix);

    if (!claim || claim.revoked) {
      return false;
    }

    if (claim.owner !== owner) {
      return false; // Can't release someone else's claim
    }

    // Mark as revoked (keep for audit trail)
    claim.revoked = true;
    claim.revokedAt = Date.now();
    claim.revocationReason = reason;

    // Remove from owner index
    const ownerPrefixes = this.ownerIndex.get(owner);
    if (ownerPrefixes) {
      ownerPrefixes.delete(normalizedPrefix);
      if (ownerPrefixes.size === 0) {
        this.ownerIndex.delete(owner);
      }
    }

    // Remove from delegation index
    if (claim.delegatedBy) {
      const delegatedPrefixes = this.delegationIndex.get(claim.delegatedBy);
      if (delegatedPrefixes) {
        delegatedPrefixes.delete(normalizedPrefix);
        if (delegatedPrefixes.size === 0) {
          this.delegationIndex.delete(claim.delegatedBy);
        }
      }
    }

    return true;
  }

  /**
   * Check if a component can create/operate at a given topic.
   *
   * Butler Lampson (Turing Award, Capability Systems):
   *   "In strict mode, all operations require explicit authority.
   *    In open mode, unclaimed territory is accessible to all."
   */
  canCreate(topic: string, owner: string): boolean {
    const authority = this.getAuthority(topic);
    if (!authority) {
      // No one owns this prefix - behavior depends on strictMode
      if (this.config.strictMode) {
        // Strict mode: unclaimed territory is forbidden
        return false;
      }
      // Open mode: unclaimed territory is allowed
      return true;
    }

    // Owner of the authority can create
    if (authority.owner === owner) {
      return true;
    }

    // Check if owner has their own claim that covers this topic
    const ownerAuthority = this.findAuthoritativePrefix(topic, owner);
    return ownerAuthority !== undefined;
  }

  /**
   * Check if a component can operate on a topic with specific capability.
   *
   * Mark Miller (Capability Security, E Language):
   *   "Capability checks must be explicit. In strict mode, no implicit authority."
   */
  canOperate(topic: string, owner: string, capability: string): boolean {
    // First check if owner can create at all
    if (!this.canCreate(topic, owner)) {
      return false;
    }

    // Find the owner's authoritative claim for this topic
    const authority = this.findAuthoritativePrefix(topic, owner);
    if (!authority) {
      // No claim, but canCreate was true - this means open territory (strictMode=false)
      // In open territory, all capabilities are implicitly allowed
      return true;
    }

    // Check if the capability is granted
    const claim = this.claims.get(authority);
    if (!claim || claim.revoked) {
      return false;
    }

    return claim.capabilities.includes(capability as CapabilityRight);
  }

  /**
   * Get the claim for a specific prefix (exact match).
   */
  getClaim(prefix: string): ITopicClaim | undefined {
    const normalizedPrefix = this.normalizePrefix(prefix);
    const claim = this.claims.get(normalizedPrefix);
    return claim && !claim.revoked ? claim : undefined;
  }

  /**
   * Get the authoritative claim for a topic (finds covering prefix).
   */
  getAuthority(topic: string): ITopicClaim | undefined {
    const normalizedTopic = this.normalizePrefix(topic);

    // Find the most specific (longest) prefix that covers this topic
    let bestClaim: ITopicClaim | undefined;
    let bestLength = -1;

    for (const [prefix, claim] of this.claims) {
      if (claim.revoked) continue;

      // Check if this prefix covers the topic
      if (this.prefixCovers(prefix, normalizedTopic)) {
        if (prefix.length > bestLength) {
          bestLength = prefix.length;
          bestClaim = claim;
        }
      }
    }

    return bestClaim;
  }

  /**
   * Get all claims owned by a component.
   */
  getClaimsByOwner(owner: string): ITopicClaim[] {
    const prefixes = this.ownerIndex.get(owner);
    if (!prefixes) {
      return [];
    }

    const result: ITopicClaim[] = [];
    for (const prefix of prefixes) {
      const claim = this.claims.get(prefix);
      if (claim && !claim.revoked) {
        result.push(claim);
      }
    }
    return result;
  }

  /**
   * Get all claims matching a query.
   */
  query(q: IClaimQuery): ITopicClaim[] {
    const results: ITopicClaim[] = [];

    for (const claim of this.claims.values()) {
      // Filter by revoked status
      if (!q.includeRevoked && claim.revoked) {
        continue;
      }

      // Filter by owner
      if (q.owner && claim.owner !== q.owner) {
        continue;
      }

      // Filter by exact prefix
      if (q.prefix && claim.prefix !== q.prefix) {
        continue;
      }

      // Filter by prefix pattern (startsWith)
      if (q.prefixPattern && !claim.prefix.startsWith(q.prefixPattern)) {
        continue;
      }

      results.push(claim);
    }

    return results;
  }

  /**
   * Get all active (non-revoked) claims.
   */
  getAllClaims(): ITopicClaim[] {
    const results: ITopicClaim[] = [];
    for (const claim of this.claims.values()) {
      if (!claim.revoked) {
        results.push(claim);
      }
    }
    return results;
  }

  /**
   * Revoke all claims owned by a component.
   *
   * Joe Armstrong (Erlang, "Programming Erlang"):
   *   "When a supervisor dies, all its children must be notified.
   *    The cascade must be complete - no orphaned authority."
   */
  revokeAll(owner: string, reason = 'component_destroyed'): number {
    const prefixes = this.ownerIndex.get(owner);
    if (!prefixes) {
      return 0;
    }

    let count = 0;

    // Copy to array to avoid mutation during iteration
    const prefixList = [...prefixes];

    for (const prefix of prefixList) {
      if (this.release(prefix, owner, reason)) {
        count++;
      }
    }

    // Also revoke claims delegated by this owner (cascade)
    count += this.cascadeRevokeDelegations(owner);

    return count;
  }

  /**
   * Recursively revoke all claims delegated by an owner.
   *
   * When A delegates to B and B delegates to C:
   * - Revoking A should cascade to B (parent_revoked)
   * - Which should cascade to C (parent_revoked)
   *
   * This ensures the full delegation chain is invalidated.
   */
  private cascadeRevokeDelegations(delegator: string): number {
    const delegatedPrefixes = this.delegationIndex.get(delegator);
    if (!delegatedPrefixes || delegatedPrefixes.size === 0) {
      return 0;
    }

    let count = 0;
    const delegatedList = [...delegatedPrefixes];

    for (const prefix of delegatedList) {
      const claim = this.claims.get(prefix);
      if (claim && !claim.revoked) {
        // Mark claim as revoked
        claim.revoked = true;
        claim.revokedAt = Date.now();
        claim.revocationReason = 'parent_revoked';
        count++;

        // Remove from owner index
        const delegatedOwnerPrefixes = this.ownerIndex.get(claim.owner);
        if (delegatedOwnerPrefixes) {
          delegatedOwnerPrefixes.delete(prefix);
          if (delegatedOwnerPrefixes.size === 0) {
            this.ownerIndex.delete(claim.owner);
          }
        }

        // RECURSIVE: Cascade to claims that THIS claim's owner delegated
        count += this.cascadeRevokeDelegations(claim.owner);
      }
    }

    this.delegationIndex.delete(delegator);
    return count;
  }

  /**
   * Check if a topic falls under any claimed prefix.
   */
  isUnderAuthority(topic: string): boolean {
    return this.getAuthority(topic) !== undefined;
  }

  /**
   * Clear all claims (for testing purposes).
   */
  clear(): void {
    this.claims.clear();
    this.ownerIndex.clear();
    this.delegationIndex.clear();
  }

  // ═══════════════════════════════════════════════════════════════════════════
  // PRIVATE HELPERS
  // ═══════════════════════════════════════════════════════════════════════════

  /**
   * Normalize a prefix by removing trailing dots.
   */
  private normalizePrefix(prefix: string): string {
    return prefix.replace(/\.+$/, '');
  }

  /**
   * Check if a prefix covers a topic.
   *
   * 'flowpad' covers 'flowpad', 'flowpad.tree', 'flowpad.tree.node'
   * 'flowpad.tree' does NOT cover 'flowpad' or 'flowpadExtra'
   */
  private prefixCovers(prefix: string, topic: string): boolean {
    if (prefix === topic) {
      return true; // Exact match
    }
    // Topic must start with prefix followed by a dot
    return topic.startsWith(prefix + '.');
  }

  /**
   * Find the authoritative prefix that a specific owner has for a topic.
   */
  private findAuthoritativePrefix(topic: string, owner: string): string | undefined {
    const normalizedTopic = this.normalizePrefix(topic);

    // Check all claims by this owner for one that covers the topic
    const ownerPrefixes = this.ownerIndex.get(owner);
    if (!ownerPrefixes) {
      return undefined;
    }

    let bestPrefix: string | undefined;
    let bestLength = -1;

    for (const prefix of ownerPrefixes) {
      const claim = this.claims.get(prefix);
      if (!claim || claim.revoked) continue;

      if (this.prefixCovers(prefix, normalizedTopic)) {
        if (prefix.length > bestLength) {
          bestLength = prefix.length;
          bestPrefix = prefix;
        }
      }
    }

    return bestPrefix;
  }

  /**
   * Find a conflicting claim for a new prefix.
   *
   * Conflicts:
   * 1. New prefix is ancestor of existing exclusive claim by different owner
   * 2. New prefix is descendant of existing exclusive claim by different owner
   *
   * Note: If delegatedBy is set, we skip conflict checking with:
   * - The delegator's own claims
   * - Claims that are ancestors of the delegator's authority (delegation chain)
   *
   * This allows nested delegation: if A delegates to B, and B delegates to C,
   * C's claim should not conflict with A's claim because B's intermediate
   * authority has already "carved out" that subtree.
   */
  private findConflictingClaim(
    prefix: string,
    owner: string,
    exclusive: boolean,
    delegatedBy?: string
  ): ITopicClaim | undefined {
    if (!exclusive) {
      return undefined; // Non-exclusive claims don't conflict
    }

    // Find the delegator's authoritative prefix if delegation is involved
    let delegatorAuthority: string | undefined;
    if (delegatedBy) {
      delegatorAuthority = this.findAuthoritativePrefix(prefix, delegatedBy);
    }

    for (const [existingPrefix, claim] of this.claims) {
      if (claim.revoked) continue;
      if (claim.owner === owner) continue; // Same owner, no conflict
      if (!claim.exclusive) continue; // Non-exclusive existing claims don't conflict

      // If this claim is owned by the delegator, skip - delegation is authorized
      if (delegatedBy && claim.owner === delegatedBy) continue;

      // If delegator has authority and existing claim is an ancestor of that authority,
      // skip - the delegation chain has already carved out this subtree
      if (delegatorAuthority && this.prefixCovers(existingPrefix, delegatorAuthority)) {
        continue;
      }

      // Check if new prefix would conflict
      if (this.prefixCovers(prefix, existingPrefix)) {
        // New prefix is ancestor of existing - would take authority
        return claim;
      }
      if (this.prefixCovers(existingPrefix, prefix)) {
        // New prefix is descendant of existing - existing has authority
        return claim;
      }
    }

    return undefined;
  }

  /**
   * Merge two capability arrays (union).
   */
  private mergeCapabilities(
    existing: CapabilityRight[],
    newCaps: CapabilityRight[]
  ): CapabilityRight[] {
    const merged = new Set<CapabilityRight>(existing);
    for (const cap of newCaps) {
      merged.add(cap);
    }
    return [...merged];
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// GLOBAL SINGLETON - REMOVED
// ═══════════════════════════════════════════════════════════════════════════
//
// Expert Panel Decision (2024-12):
// Global singleton pattern REMOVED per security review.
//
// Mark Miller (Capability Security, E Language):
//   "Global registries are ambient authority. In capability systems,
//    every authority must be explicitly passed, never implicitly available."
//
// Butler Lampson (Turing Award, Capability Systems):
//   "The registry should be PASSED, not FETCHED. Injection enables
//    distributed implementations (Raft, Paxos) without code changes."
//
// Joe Armstrong (Erlang, "Programming Erlang"):
//   "In Erlang, we pass the registry PID explicitly. No globals."
//
// NEW PATTERN:
// - MatrixRuntime: config.topicClaimRegistry (creates new instance if not provided)
// - MatrixRuntime: config.topicClaimRegistry (same pattern)
// - Tests: Create new TopicClaimRegistry() per test for isolation
//
// MIGRATION:
// - OLD: const reg = getGlobalTopicClaimRegistry();
// - NEW: const reg = new TopicClaimRegistry(); // or inject via config
//
// ═══════════════════════════════════════════════════════════════════════════
