#!/usr/bin/env node
/**
 * OpenClaw Governance — Budget Tracker + Permission Mode
 *
 * Budget tracking (token/cost per session + global).
 * Permission modes (plan/auto, allowed/disallowed tools).
 * Audit logging for compliance.
 *
 * Migrated from ECC:
 *   ecc2/src/config/mod.rs (budget fields, permission_mode)
 *   ecc2/src/observability/mod.rs (ToolLogEntry)
 *
 * Usage:
 *   const { BudgetTracker, PermissionGate, AuditLog } = require('./governance');
 */

const fs = require('fs');
const path = require('path');

// ─── Budget Tracker ──────────────────────────────────────────────

/**
 * Track token and cost budgets per session and globally.
 */
class BudgetTracker {
  constructor(options = {}) {
    this.perSession = {
      tokenBudget: options.sessionTokenBudget || 500000,    // 500K tokens
      costBudget: options.sessionCostBudget || 5.0,         // $5 USD
      hardLimit: options.sessionHardLimit !== false,
    };
    this.global = {
      tokenBudget: options.globalTokenBudget || 5000000,    // 5M tokens/day
      costBudget: options.globalCostBudget || 50.0,         // $50/day
      hardLimit: options.globalHardLimit !== false,
    };
    this.usage = {
      sessions: new Map(),  // sessionId → { tokens, cost }
      global: { tokens: 0, cost: 0 },
      resetAt: this._nextReset(),
    };
    this.alerts = [];
  }

  /**
   * Record token/cost usage for a tool call.
   * @returns {object} { allowed, remaining, alert? }
   */
  recordUsage(sessionId, tokens, cost = 0) {
    // Update session usage
    if (!this.usage.sessions.has(sessionId)) {
      this.usage.sessions.set(sessionId, { tokens: 0, cost: 0 });
    }
    const session = this.usage.sessions.get(sessionId);
    session.tokens += tokens;
    session.cost += cost;

    // Update global usage
    this.usage.global.tokens += tokens;
    this.usage.global.cost += cost;

    // Check limits
    const result = {
      allowed: true,
      session: {
        used: session.tokens,
        budget: this.perSession.tokenBudget,
        percent: Math.round(session.tokens / this.perSession.tokenBudget * 100),
      },
      global: {
        used: this.usage.global.tokens,
        budget: this.global.tokenBudget,
        percent: Math.round(this.usage.global.tokens / this.global.tokenBudget * 100),
      },
    };

    // Session limit check
    if (session.tokens >= this.perSession.tokenBudget) {
      if (this.perSession.hardLimit) {
        result.allowed = false;
        result.blocked = 'session_token_limit';
      }
      this._alert(sessionId, `Session token budget exceeded: ${session.tokens}/${this.perSession.tokenBudget}`);
    }

    if (session.cost >= this.perSession.costBudget) {
      if (this.perSession.hardLimit) {
        result.allowed = false;
        result.blocked = 'session_cost_limit';
      }
      this._alert(sessionId, `Session cost budget exceeded: $${session.cost.toFixed(2)}/$${this.perSession.costBudget}`);
    }

    // Global limit check
    if (this.usage.global.tokens >= this.global.tokenBudget) {
      if (this.global.hardLimit) {
        result.allowed = false;
        result.blocked = result.blocked || 'global_token_limit';
      }
      this._alert('global', `Global token budget exceeded: ${this.usage.global.tokens}/${this.global.tokenBudget}`);
    }

    return result;
  }

  /**
   * Check remaining budget without recording.
   */
  checkBudget(sessionId, estimatedTokens) {
    const session = this.usage.sessions.get(sessionId);
    const sessionUsed = session ? session.tokens : 0;

    return {
      sessionRemaining: this.perSession.tokenBudget - sessionUsed - estimatedTokens,
      globalRemaining: this.global.tokenBudget - this.usage.global.tokens - estimatedTokens,
      sessionWarn: sessionUsed / this.perSession.tokenBudget > 0.8,
      globalWarn: this.usage.global.tokens / this.global.tokenBudget > 0.8,
    };
  }

  /**
   * Reset daily global budget.
   */
  dailyReset() {
    this.usage.global.tokens = 0;
    this.usage.global.cost = 0;
    this.usage.resetAt = this._nextReset();
  }

  _alert(sessionId, message) {
    this.alerts.push({
      sessionId,
      message,
      timestamp: new Date().toISOString(),
    });
  }

  _nextReset() {
    const now = new Date();
    return new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0).toISOString();
  }

  /**
   * Get budget report.
   */
  report() {
    const sessions = [...this.usage.sessions.entries()].map(([id, u]) => ({
      sessionId: id,
      tokens: u.tokens,
      cost: u.cost,
      percent: Math.round(u.tokens / this.perSession.tokenBudget * 100),
    }));

    return {
      global: {
        used: this.usage.global.tokens,
        budget: this.global.tokenBudget,
        percent: Math.round(this.usage.global.tokens / this.global.tokenBudget * 100),
        resetsAt: this.usage.resetAt,
      },
      sessions,
      alerts: this.alerts.slice(-20),
    };
  }
}

// ─── Permission Gate ─────────────────────────────────────────────

/**
 * Permission mode and tool allow/deny lists.
 * ECC: AgentProfileConfig.permission_mode + allowed_tools + disallowed_tools
 */
class PermissionGate {
  constructor(options = {}) {
    this.mode = options.mode || 'auto';      // 'plan' | 'auto'
    this.allowedTools = options.allowedTools || null;  // null = all allowed
    this.disallowedTools = options.disallowedTools || [];
    this.requireConfirmation = options.requireConfirmation || [];  // tools that always need confirm
    this.blockedTools = options.blockedTools || [];               // tools that are always blocked
  }

  /**
   * Check if a tool call is allowed.
   * @returns {object} { allowed, action, reason }
   */
  check(toolName) {
    // 1. Blocked tools
    if (this.blockedTools.includes(toolName)) {
      return { allowed: false, action: 'block', reason: `Tool '${toolName}' is blocked` };
    }

    // 2. Disallowed tools
    if (this.disallowedTools.includes(toolName)) {
      return { allowed: false, action: 'block', reason: `Tool '${toolName}' is disallowed` };
    }

    // 3. Allowlist check (if set)
    if (this.allowedTools && !this.allowedTools.includes(toolName)) {
      return { allowed: false, action: 'block', reason: `Tool '${toolName}' not in allowlist` };
    }

    // 4. Confirmation required
    if (this.requireConfirmation.includes(toolName)) {
      return { allowed: true, action: 'confirm', reason: `Tool '${toolName}' requires confirmation` };
    }

    // 5. Plan mode: all tools except Read/Grep/Glob need confirmation
    if (this.mode === 'plan') {
      const autoAllow = ['Read', 'read', 'Grep', 'Glob', 'memory_get', 'memory_search', 'web_search', 'ctx_search'];
      if (!autoAllow.includes(toolName)) {
        return { allowed: true, action: 'confirm', reason: `Plan mode: '${toolName}' requires confirmation` };
      }
    }

    return { allowed: true, action: 'allow', reason: 'OK' };
  }

  /**
   * Set permission mode.
   */
  setMode(mode) {
    if (!['plan', 'auto'].includes(mode)) {
      throw new Error(`Invalid permission mode: ${mode}. Use 'plan' or 'auto'.`);
    }
    this.mode = mode;
  }
}

// ─── Audit Log ───────────────────────────────────────────────────

/**
 * Audit trail for tool calls and security events.
 * ECC: ToolLogEntry (ecc2/src/observability/mod.rs:240-290)
 */
class AuditLog {
  constructor(options = {}) {
    this.logPath = options.logPath || path.join(
      process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME, '.openclaw'),
      'audit.jsonl'
    );
    this.buffer = [];
    this.bufferSize = options.bufferSize || 10;
    this._ensureDir();
  }

  /**
   * Log a tool call.
   */
  logToolCall({ sessionId, toolName, input, output, durationMs, riskScore }) {
    const entry = {
      type: 'tool_call',
      sessionId,
      toolName,
      inputSummary: this._summarize(input, 200),
      outputSummary: this._summarize(output, 100),
      durationMs: durationMs || 0,
      riskScore: riskScore || 0,
      timestamp: new Date().toISOString(),
    };
    this._write(entry);
    return entry;
  }

  /**
   * Log a security event.
   */
  logSecurity({ sessionId, event, details, severity = 'info' }) {
    const entry = {
      type: 'security',
      sessionId,
      event,
      details,
      severity,
      timestamp: new Date().toISOString(),
    };
    this._write(entry);
    return entry;
  }

  /**
   * Log a budget event.
   */
  logBudget({ sessionId, event, tokens, cost, limit }) {
    const entry = {
      type: 'budget',
      sessionId,
      event,
      tokens,
      cost,
      limit,
      timestamp: new Date().toISOString(),
    };
    this._write(entry);
    return entry;
  }

  /**
   * Log a decision.
   */
  logDecision({ sessionId, decision, reasoning }) {
    const entry = {
      type: 'decision',
      sessionId,
      decision,
      reasoning,
      timestamp: new Date().toISOString(),
    };
    this._write(entry);
    return entry;
  }

  /**
   * Query audit log (JSONL read).
   */
  query({ type, sessionId, since, limit = 50 } = {}) {
    const results = [];

    try {
      if (!fs.existsSync(this.logPath)) return results;

      const lines = fs.readFileSync(this.logPath, 'utf8').split('\n');
      for (const line of lines.reverse()) {  // newest first
        if (!line.trim()) continue;
        try {
          const entry = JSON.parse(line);
          if (type && entry.type !== type) continue;
          if (sessionId && entry.sessionId !== sessionId) continue;
          if (since && new Date(entry.timestamp) < new Date(since)) continue;
          results.push(entry);
          if (results.length >= limit) break;
        } catch {}
      }
    } catch {}

    return results;
  }

  _write(entry) {
    this.buffer.push(entry);
    if (this.buffer.length >= this.bufferSize) this._flush();
  }

  _flush() {
    try {
      const lines = this.buffer.map(e => JSON.stringify(e)).join('\n') + '\n';
      fs.appendFileSync(this.logPath, lines, 'utf8');
      this.buffer = [];
    } catch {}
  }

  _ensureDir() {
    const dir = path.dirname(this.logPath);
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  }

  _summarize(value, maxLen = 200) {
    if (!value) return '';
    const str = typeof value === 'string' ? value : JSON.stringify(value);
    return str.length > maxLen ? str.substring(0, maxLen) + '...' : str;
  }
}

// ─── Governance Pipeline ─────────────────────────────────────────

/**
 * Full governance check pipeline.
 * Combines BudgetTracker, PermissionGate, and AuditLog.
 */
class GovernancePipeline {
  constructor(options = {}) {
    this.budget = options.budget || new BudgetTracker(options.budgetOpts);
    this.permission = options.permission || new PermissionGate(options.permissionOpts);
    this.audit = options.audit || new AuditLog(options.auditOpts);
  }

  /**
   * Pre-execution check: permission + budget.
   * Called before every tool execution.
   *
   * @returns {object} { allowed, action, reason, budget }
   */
  preCheck(sessionId, toolName, input) {
    // 1. Permission check
    const perm = this.permission.check(toolName);
    if (!perm.allowed) {
      this.audit.logSecurity({
        sessionId,
        event: 'tool_blocked',
        details: `Tool '${toolName}' blocked: ${perm.reason}`,
        severity: 'warn',
      });
      return { ...perm, budget: null };
    }

    // 2. Budget check (estimate 500 tokens per tool call)
    const budget = this.budget.checkBudget(sessionId, 500);
    if (budget.sessionRemaining < 0 || budget.globalRemaining < 0) {
      this.audit.logBudget({
        sessionId,
        event: 'budget_exceeded',
        tokens: this.budget.usage.sessions.get(sessionId)?.tokens || 0,
      });
      return { allowed: false, action: 'block', reason: 'Budget exceeded', budget };
    }

    return { ...perm, budget };
  }

  /**
   * Post-execution recording: usage + audit.
   * Called after every tool execution.
   */
  postRecord(sessionId, toolName, input, output, durationMs, riskScore) {
    // Record usage
    const estimateTokens = Math.ceil(
      (typeof input === 'string' ? input.length : JSON.stringify(input || '').length) / 3.5
    );
    this.budget.recordUsage(sessionId, estimateTokens);

    // Audit log
    this.audit.logToolCall({
      sessionId, toolName, input, output, durationMs, riskScore,
    });
  }
}

// ─── Module exports ──────────────────────────────────────────────

module.exports = {
  BudgetTracker,
  PermissionGate,
  AuditLog,
  GovernancePipeline,
};
