import type { AutonomyLevel, ToolsConfig, ToolProfile } from '../config/schema.js';
import type { Tool } from './types.js';
import type { SkillPermissions } from '../skills/types.js';

// ── Profile definitions ─────────────────────────────────────────────

/** Built-in tool name sets for each profile level. Profiles are cumulative. */
const PROFILE_TOOLS: Record<ToolProfile, Set<string>> = {
  minimal: new Set([
    'file.read',
    'file.list',
    'web.fetch',
    'system.info',
  ]),
  messaging: new Set([
    'file.read',
    'file.list',
    'web.fetch',
    'system.info',
    'memory.read',
    'memory.write',
    'memory.search',
    'web.search',
    'gmail.list',
    'gmail.search',
    'gmail.read',
    'calendar.today',
    'calendar.list',
    'calendar.search',
    'docs.list',
    'docs.search',
    'docs.read',
    'drive.list',
    'drive.search',
    'drive.read',
    'tasks.lists',
    'tasks.list',
    'cron.list',
    'cron.trigger',
    'cron.create',
    'cron.delete',
  ]),
  coding: new Set([
    'file.read',
    'file.list',
    'web.fetch',
    'system.info',
    'memory.read',
    'memory.write',
    'memory.search',
    'web.search',
    'gmail.list',
    'gmail.search',
    'gmail.read',
    'calendar.today',
    'calendar.list',
    'calendar.search',
    'docs.list',
    'docs.search',
    'docs.read',
    'drive.list',
    'drive.search',
    'drive.read',
    'tasks.lists',
    'tasks.list',
    'cron.list',
    'cron.trigger',
    'cron.create',
    'cron.delete',
    'file.write',
    'file.edit',
    'file.patch',
    'shell.exec',
    'process.start',
    'process.status',
    'process.output',
    'process.kill',
    'process.list',
    'browser.navigate',
    'browser.screenshot',
    'browser.click',
    'browser.type',
    'browser.content',
    'browser.eval',
  ]),
  full: new Set(), // Special: matches everything
};

// ── Tool groups ─────────────────────────────────────────────────────

/** Named groups for use in allow/deny lists (e.g. 'group:fs'). */
export const TOOL_GROUPS: Record<string, string[]> = {
  'group:fs': ['file.read', 'file.write', 'file.edit', 'file.patch', 'file.list'],
  'group:runtime': ['shell.exec', 'process.start', 'process.output', 'process.status', 'process.kill', 'process.list'],
  'group:web': ['web.fetch', 'web.search', 'browser.navigate', 'browser.screenshot', 'browser.click', 'browser.type', 'browser.content', 'browser.eval'],
  'group:memory': ['memory.read', 'memory.write', 'memory.search'],
  'group:gmail': ['gmail.list', 'gmail.search', 'gmail.read'],
  'group:gcal': ['calendar.today', 'calendar.list', 'calendar.search'],
  'group:gdocs': ['docs.list', 'docs.search', 'docs.read'],
  'group:gdrive': ['drive.list', 'drive.search', 'drive.read'],
  'group:gtasks': ['tasks.lists', 'tasks.list'],
  'group:cron': ['cron.list', 'cron.trigger', 'cron.create', 'cron.delete'],
};

/** Expand group references in a list of tool names/patterns. */
function expandGroups(names: string[]): string[] {
  return names.flatMap(n => TOOL_GROUPS[n] ?? [n]);
}

// ── Glob matching ───────────────────────────────────────────────────

/**
 * Convert a simple glob pattern to a regex.
 * Supports `*` (matches any characters) and `.` is escaped.
 * Same algorithm as HookEngine.patternToRegex for consistency.
 */
function patternToRegex(pattern: string): RegExp {
  const escaped = pattern
    .replace(/[.+^${}()|[\]\\]/g, '\\$&')
    .replace(/\*/g, '.*');
  return new RegExp(`^${escaped}$`);
}

function matchesAnyPattern(toolName: string, patterns: string[]): boolean {
  return patterns.some(p => patternToRegex(p).test(toolName));
}

// ── Policy context ──────────────────────────────────────────────────

/** Identifies the runtime context for tool policy resolution. */
export interface ToolPolicyContext {
  /** Model tier name (e.g. 'fast', 'default', 'complex', 'local'). */
  agent?: string;
  /** Provider name (e.g. 'ollama', 'anthropic'). */
  provider?: string;
  /** Session ID for audit logging. */
  sessionId?: string;
  /** Channel name for audit logging. */
  channel?: string;
  /** Sender ID for audit logging. */
  sender?: string;
  /** Model tier for audit logging. */
  tier?: string;
  /** Autonomy level for tool execution (affects confirmation requirements). */
  autonomyLevel?: AutonomyLevel;

  /** Optional active skill name (for capability scoping and audit). */
  skillName?: string;
  /** Optional active skill permissions (capability declarations). */
  skillPermissions?: SkillPermissions;

  /** Execution environment for high-risk operations. */
  executionEnvironment?: 'host' | 'sandbox';

  /** Secret scopes allowed for this context (used by executor). */
  allowedSecretScopes?: string[];

  /** True when untrusted content has been introduced in this run. */
  untrustedContent?: boolean;

  /** Elevated mode (break-glass): allow host execution for high-risk tools until this epoch millis. */
  elevatedHostUntilMs?: number;
  /** User-supplied reason for elevation (audited separately). */
  elevatedHostReason?: string;
  /** Correlation id for elevation window. */
  elevatedHostId?: string;
}

function resolveSkillAllowedNames(allToolNames: string[], permissions?: SkillPermissions): Set<string> | null {
  if (!permissions) {
    return null;
  }

  const explicitTools = Array.isArray(permissions.tools) ? permissions.tools : undefined;
  const toolGroups = Array.isArray(permissions.tool_groups) ? permissions.tool_groups : undefined;

  const patterns = (explicitTools && explicitTools.length > 0)
    ? expandGroups(explicitTools)
    : (toolGroups && toolGroups.length > 0)
      ? expandGroups(toolGroups)
      : [];

  if (patterns.length === 0) {
    return null;
  }

  return new Set(allToolNames.filter((name) => matchesAnyPattern(name, patterns)));
}

// ── ToolPolicy engine ───────────────────────────────────────────────

/**
 * Resolves which tools are permitted for a given runtime context.
 *
 * Resolution order:
 * 1. Start with profile's tool set (or all tools for 'full')
 * 2. Apply global allow list (adds tools back in)
 * 3. Apply global deny list (removes tools)
 * 4. If agent/provider overrides exist, compute their resolved sets
 *    and intersect with the global set
 * 5. Deny always wins over allow at every level
 */
export class ToolPolicy {
  private config: ToolsConfig;

  constructor(config: ToolsConfig) {
    this.config = config;
  }

  /**
   * Return the list of tools permitted for the given context.
   * This is the primary API — filters an array of Tool objects.
   */
  filterTools(tools: Tool[], context?: ToolPolicyContext): Tool[] {
    const allowed = this.resolveAllowedNames(
      tools.map(t => t.name),
      context,
    );
    return tools.filter(t => allowed.has(t.name));
  }

  /**
   * Check whether a specific tool name is permitted in the given context.
   * Used for runtime enforcement in the executor (defense in depth).
   */
  isAllowed(toolName: string, allToolNames: string[], context?: ToolPolicyContext): boolean {
    const allowed = this.resolveAllowedNames(allToolNames, context);
    return allowed.has(toolName);
  }

  /**
   * Resolve the full set of allowed tool names given an array of all
   * registered tool names and an optional context.
   */
  resolveAllowedNames(allToolNames: string[], context?: ToolPolicyContext): Set<string> {
    // Step 1: Start from global profile
    let allowed = this.applyProfile(this.config.profile, allToolNames);

    // Step 2: Apply global allow (adds tools) — expand groups first
    const globalAllow = expandGroups(this.config.allow);
    if (globalAllow.length > 0) {
      for (const name of allToolNames) {
        if (matchesAnyPattern(name, globalAllow)) {
          allowed.add(name);
        }
      }
    }

    // Step 3: Apply global deny (removes tools) — expand groups first
    const globalDeny = expandGroups(this.config.deny);
    if (globalDeny.length > 0) {
      allowed = new Set(
        [...allowed].filter(name => !matchesAnyPattern(name, globalDeny)),
      );
    }

    // Step 4: Apply agent override if present
    if (context?.agent && this.config.agents[context.agent]) {
      const agentOverride = this.config.agents[context.agent];
      const agentAllowed = this.resolveOverride(agentOverride, allToolNames);
      allowed = intersect(allowed, agentAllowed);
    }

    // Step 5: Apply provider override if present
    if (context?.provider && this.config.providers[context.provider]) {
      const providerOverride = this.config.providers[context.provider];
      const providerAllowed = this.resolveOverride(providerOverride, allToolNames);
      allowed = intersect(allowed, providerAllowed);
    }

    // Step 6: Apply skill capability restrictions (intersection)
    // Safe-by-default: if a skill is active but has no permissions manifest, deny all tools.
    if (context?.skillName && !context.skillPermissions) {
      allowed = new Set();
    } else {
      const skillAllowed = resolveSkillAllowedNames(allToolNames, context?.skillPermissions);
      if (skillAllowed) {
        allowed = intersect(allowed, skillAllowed);
      }
    }

    return allowed;
  }

  /**
   * Get the effective profile for a given context.
   * Used for informational/debugging purposes.
   */
  getEffectiveProfile(context?: ToolPolicyContext): ToolProfile {
    // Check agent override first, then provider, then global
    if (context?.agent && this.config.agents[context.agent]?.profile) {
      return this.config.agents[context.agent].profile!;
    }
    if (context?.provider && this.config.providers[context.provider]?.profile) {
      return this.config.providers[context.provider].profile!;
    }
    return this.config.profile;
  }

  // ── Private helpers ─────────────────────────────────────────────────

  /**
   * Resolve the tool set for a profile.
   * 'full' means all tools; other profiles return their defined set,
   * filtered to only include actually-registered tools.
   */
  private applyProfile(profile: ToolProfile, allToolNames: string[]): Set<string> {
    if (profile === 'full') {
      return new Set(allToolNames);
    }
    const profileSet = PROFILE_TOOLS[profile];
    return new Set(allToolNames.filter(name => profileSet.has(name)));
  }

  /**
   * Resolve an override block (agent or provider) to a set of allowed names.
   * An override inherits from the global profile if it doesn't specify its own.
   */
  private resolveOverride(
    override: { profile?: ToolProfile; allow: string[]; deny: string[] },
    allToolNames: string[],
  ): Set<string> {
    // Start from the override's profile, or inherit global
    const baseProfile = override.profile ?? this.config.profile;
    let allowed = this.applyProfile(baseProfile, allToolNames);

    // Apply override allow — expand groups first
    const overrideAllow = expandGroups(override.allow);
    if (overrideAllow.length > 0) {
      for (const name of allToolNames) {
        if (matchesAnyPattern(name, overrideAllow)) {
          allowed.add(name);
        }
      }
    }

    // Apply override deny (deny always wins) — expand groups first
    const overrideDeny = expandGroups(override.deny);
    if (overrideDeny.length > 0) {
      allowed = new Set(
        [...allowed].filter(name => !matchesAnyPattern(name, overrideDeny)),
      );
    }

    return allowed;
  }
}

// ── Utility ─────────────────────────────────────────────────────────

function intersect(a: Set<string>, b: Set<string>): Set<string> {
  const result = new Set<string>();
  for (const item of a) {
    if (b.has(item)) {
      result.add(item);
    }
  }
  return result;
}

/**
 * Exported for testing and for use in HookEngine (DRY).
 */
export { patternToRegex, matchesAnyPattern, PROFILE_TOOLS, expandGroups };
