// tools/skill-scope-verifier.mjs
/**
 * Skill scope verification — router-gate v4 spec §3.7 + v4.1 (Direction 2).
 * Pure: трекинг tools после Skill match, off-scope ratio, content-level scope.
 * v4.1: hard-block at 30% off-scope (was 50%), content-level file-path check.
 */
import { globMatch } from './path-normalization.mjs';

export const V4_1_SCOPE_THRESHOLDS = Object.freeze({
  off_scope_warn_ratio: 0.15,
  off_scope_hard_ratio: 0.30,
  content_level_scope_check: true,
  min_tools_for_evaluation: 3,
});

const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);

export function initTracker({ skillInvoked, turnId, toolUseIdOfSkill, scopeConfig, tsSkillInvokedIso }) {
  return {
    skill_invoked: skillInvoked,
    turn_id: turnId,
    tool_use_id_of_skill: toolUseIdOfSkill,
    scope_config: scopeConfig,
    tool_count_since_skill: 0,
    off_scope_count: 0,
    expected_output_written: false,
    ts_skill_invoked: tsSkillInvokedIso,
  };
}

export function updateTracker(tracker, toolName, toolInput = {}) {
  const cfg = tracker.scope_config || {};
  const expected = cfg.expected_tools || [];
  const offScope = expected.length > 0 && !expected.includes(toolName);

  let outputWritten = tracker.expected_output_written;
  if (WRITE_TOOLS.has(toolName) && cfg.expected_output_glob && toolInput.file_path) {
    if (globMatch(toolInput.file_path, cfg.expected_output_glob)) outputWritten = true;
  }

  return {
    ...tracker,
    tool_count_since_skill: tracker.tool_count_since_skill + 1,
    off_scope_count: tracker.off_scope_count + (offScope ? 1 : 0),
    expected_output_written: outputWritten,
  };
}

export function evaluateScope(tracker, thresholds = V4_1_SCOPE_THRESHOLDS) {
  if (tracker.tool_count_since_skill < thresholds.min_tools_for_evaluation) {
    return { action: 'allow' };
  }
  const ratio = tracker.off_scope_count / tracker.tool_count_since_skill;
  if (ratio >= thresholds.off_scope_hard_ratio) {
    return {
      action: 'hard_block',
      reason: 'Skill scope hard-block (v4.1): off-scope tools >30%. Skill mismatch — invoke matching Skill или AskUser approval для scope-broadening.',
    };
  }
  if (ratio >= thresholds.off_scope_warn_ratio) {
    return { action: 'soft_flag', surface_in_next_prompt: true };
  }
  return { action: 'allow' };
}

export function contentScopeCheck(toolName, filePath, scopeConfig) {
  if (!WRITE_TOOLS.has(toolName)) return { action: 'allow' };
  const cs = scopeConfig?.content_scope?.[toolName];
  if (!cs || !cs.allowed_globs) return { action: 'allow' };
  if (!filePath) return { action: 'allow' };
  const ok = cs.allowed_globs.some((g) => globMatch(filePath, g));
  if (ok) return { action: 'allow' };
  return {
    action: 'hard_block',
    reason: 'v4.1 content-level scope: file path не соответствует Skill expected output domain',
  };
}

export function maxCallsCheck(tracker) {
  const max = tracker.scope_config?.max_tool_calls_post_skill;
  if (max && tracker.tool_count_since_skill >= max && !tracker.expected_output_written) {
    return {
      action: 'hard_block',
      reason: `Skill scope exceeded (>=${max} tools post-skill без expected output). Требуется new Skill match ИЛИ AskUser approval.`,
    };
  }
  return { action: 'allow' };
}
