/**
 * 多策略匹配引擎（纯函数、无 I/O）。
 *
 * 职责：给定一次观测到的工具调用（toolName + params）和「本 session 已激活的 skill 集合」，
 * 判定它是否对应某 skill 的某功能点，并解析出参数。
 *
 * 四种策略（由标准配置里 `match.type` 判别）：
 *   - script  ：exec 跑 skill 自带脚本（.py/.sh/.js），按脚本路径后缀匹配
 *   - command ：exec 调包装 CLI（如 `mcporter call linear.list_issues`）
 *   - tool    ：直接调非 exec 工具（MCP/SSE 工具），按 toolName(+参数谓词) 匹配
 *   - http    ：curl/fetch 打 HTTP/SSE 端点，按 url/host 子串匹配
 *
 * 为保证 hook 热路径快：先用 buildIndex 预索引（按 basename / 命令头 / toolName / host 建桶），
 * match 时仅在小候选集上做精确判定。
 */
import path from "node:path";
import type {
  ArgRule,
  CommandMatchRule,
  HttpMatchRule,
  MatchResult,
  ScriptMatchRule,
  SkillStandardConfig,
  ToolCall,
  ToolMatchRule,
  WherePredicate,
} from "./types.ts";

/** 索引里每个功能点附带其所属 skill 信息。 */
type IndexedFn = {
  skillName: string;
  skillVersion: string;
  functionId: string;
  functionName: string;
  rule: ScriptMatchRule | CommandMatchRule | ToolMatchRule | HttpMatchRule;
  /** tool 规则的 toolNameRegex 预编译结果：RegExp 命中、null 表示编译失败、undefined 表示无正则。 */
  compiledRegex?: RegExp | null;
};

export type MatchIndex = {
  /** script：按脚本 basename 建桶。 */
  scriptByBasename: Map<string, IndexedFn[]>;
  /** command：按命令头建桶。 */
  commandByHead: Map<string, IndexedFn[]>;
  /** tool：精确 toolName 建桶。 */
  toolByName: Map<string, IndexedFn[]>;
  /** tool：前缀/正则规则（无法用精确 key 命中，逐个判定）。 */
  toolFuzzy: IndexedFn[];
  /** http：规则量少，整体扫描。 */
  httpRules: IndexedFn[];
};

/** 已知解释器/包装前缀，用于在找「命令头」时跳过。 */
const INTERPRETERS = new Set([
  "python", "python3", "py", "bash", "sh", "zsh", "node", "ts-node",
  "tsx", "deno", "ruby", "perl", "uv", "uvx", "npx", "pnpm", "yarn", "env",
]);

/** 空索引。 */
export function emptyIndex(): MatchIndex {
  return {
    scriptByBasename: new Map(),
    commandByHead: new Map(),
    toolByName: new Map(),
    toolFuzzy: [],
    httpRules: [],
  };
}

function pushBucket(map: Map<string, IndexedFn[]>, key: string, fn: IndexedFn): void {
  const arr = map.get(key);
  if (arr) arr.push(fn);
  else map.set(key, [fn]);
}

/** 把标准配置编译成可快速匹配的索引。 */
export function buildIndex(configs: SkillStandardConfig[]): MatchIndex {
  const index = emptyIndex();
  for (const cfg of configs) {
    for (const fn of cfg.functions) {
      const indexed: IndexedFn = {
        skillName: cfg.skillName,
        skillVersion: cfg.version,
        functionId: fn.id,
        functionName: fn.name,
        rule: fn.match,
      };
      switch (fn.match.type) {
        case "script":
          pushBucket(index.scriptByBasename, path.basename(fn.match.script), indexed);
          break;
        case "command":
          pushBucket(index.commandByHead, fn.match.command, indexed);
          break;
        case "tool":
          if (fn.match.toolName) {
            pushBucket(index.toolByName, fn.match.toolName, indexed);
          } else {
            if (fn.match.toolNameRegex) {
              try {
                indexed.compiledRegex = new RegExp(fn.match.toolNameRegex);
              } catch {
                indexed.compiledRegex = null; // 非法正则：预编译失败，匹配时直接不命中
              }
            }
            index.toolFuzzy.push(indexed);
          }
          break;
        case "http":
          index.httpRules.push(indexed);
          break;
      }
    }
  }
  return index;
}

/**
 * 把 shell 命令切成 token，识别简单的单/双引号；不求完整 shell 语义，够匹配脚本路径与 flag 即可。
 */
export function tokenize(command: string): string[] {
  const tokens: string[] = [];
  const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
  let m: RegExpExecArray | null;
  while ((m = re.exec(command)) !== null) {
    tokens.push(m[1] ?? m[2] ?? m[3] ?? "");
  }
  return tokens;
}

/** 路径后缀匹配（按路径分隔对齐，避免 `xfoo.py` 误判 `foo.py`）。 */
export function pathEndsWith(token: string, scriptRel: string): boolean {
  const t = token.replace(/\\/g, "/");
  const s = scriptRel.replace(/\\/g, "/").replace(/^\.?\//, "");
  return t === s || t.endsWith("/" + s);
}

/** 解析命令行 flag：`--k v` / `--k=v` / `-k v` / 布尔 flag。返回去前缀的键值表。 */
export function parseFlags(tokens: string[]): Record<string, unknown> {
  const out: Record<string, unknown> = {};
  for (let i = 0; i < tokens.length; i++) {
    const t = tokens[i];
    if (t.startsWith("--")) {
      const eq = t.indexOf("=");
      if (eq >= 0) {
        out[t.slice(2, eq)] = t.slice(eq + 1);
      } else {
        const next = tokens[i + 1];
        if (next !== undefined && !next.startsWith("-")) {
          out[t.slice(2)] = next;
          i++;
        } else {
          out[t.slice(2)] = true;
        }
      }
    } else if (t.length > 1 && t.startsWith("-") && !/^-\d/.test(t)) {
      const key = t.slice(1);
      const next = tokens[i + 1];
      if (next !== undefined && !next.startsWith("-")) {
        out[key] = next;
        i++;
      } else {
        out[key] = true;
      }
    }
  }
  return out;
}

/** argRules 全部命中才算命中（flag 去前缀比较；value 省略则只判存在）。 */
function argRulesMatch(argRules: ArgRule[] | undefined, flags: Record<string, unknown>): boolean {
  if (!argRules || argRules.length === 0) return true;
  for (const rule of argRules) {
    const key = rule.flag.replace(/^-+/, "");
    if (!(key in flags)) return false;
    if (rule.value !== undefined && String(flags[key]) !== rule.value) return false;
  }
  return true;
}

/** 找命令头：跳过 `VAR=val` 环境前缀与已知解释器，返回首个真实命令名。 */
export function commandHead(tokens: string[]): string | undefined {
  for (let i = 0; i < tokens.length; i++) {
    const t = tokens[i];
    if (/^[A-Za-z_][\w]*=/.test(t)) continue; // 环境变量前缀
    const base = path.basename(t);
    if (INTERPRETERS.has(base)) continue; // 解释器
    return base;
  }
  return undefined;
}

/** 解析 `k=v` / `k:v`（用于 mcporter 等），避开 URL 的 `://`。 */
export function parseKeyValues(tokens: string[]): Record<string, unknown> {
  const out: Record<string, unknown> = {};
  for (const t of tokens) {
    if (t.includes("://")) continue;
    const m = /^([A-Za-z_][\w.-]*)[=:](.*)$/.exec(t);
    if (m) out[m[1]] = m[2];
  }
  return out;
}

/** 从字符串里抽第一个 URL。 */
function extractUrl(s: string): string | undefined {
  const m = /https?:\/\/[^\s"'`]+/.exec(s);
  return m ? m[0] : undefined;
}

/** 解析 URL 的 query 参数为对象。 */
function parseQuery(url: string): Record<string, unknown> {
  const out: Record<string, unknown> = {};
  const qi = url.indexOf("?");
  if (qi < 0) return out;
  for (const pair of url.slice(qi + 1).split("&")) {
    if (!pair) continue;
    const eq = pair.indexOf("=");
    const k = decodeURIComponent(eq >= 0 ? pair.slice(0, eq) : pair);
    const v = eq >= 0 ? decodeURIComponent(pair.slice(eq + 1)) : true;
    out[k] = v;
  }
  return out;
}

function httpMatchesUrl(url: string, rule: HttpMatchRule): boolean {
  if (rule.urlContains && !url.includes(rule.urlContains)) return false;
  if (rule.hostContains) {
    let host = "";
    try {
      host = new URL(url).host;
    } catch {
      host = url;
    }
    if (!host.includes(rule.hostContains)) return false;
  }
  return Boolean(rule.urlContains || rule.hostContains);
}

/** 点路径取嵌套字段。 */
function getByPath(obj: Record<string, unknown>, dotted: string): unknown {
  let cur: unknown = obj;
  for (const seg of dotted.split(".")) {
    if (cur && typeof cur === "object" && seg in (cur as Record<string, unknown>)) {
      cur = (cur as Record<string, unknown>)[seg];
    } else {
      return undefined;
    }
  }
  return cur;
}

function whereMatches(where: WherePredicate[] | undefined, params: Record<string, unknown>): boolean {
  if (!where || where.length === 0) return true;
  for (const p of where) {
    if (String(getByPath(params, p.param)) !== p.equals) return false;
  }
  return true;
}

function toolNameMatches(f: IndexedFn, toolName: string): boolean {
  const rule = f.rule as ToolMatchRule;
  if (rule.toolName) return rule.toolName === toolName;
  if (rule.toolNamePrefix) return toolName.startsWith(rule.toolNamePrefix);
  if (rule.toolNameRegex) {
    // 预编译命中走缓存；null 表示编译失败不命中；undefined 仅为兜底（理论上 buildIndex 必已编译）。
    if (f.compiledRegex === null) return false;
    if (f.compiledRegex) return f.compiledRegex.test(toolName);
    try {
      return new RegExp(rule.toolNameRegex).test(toolName);
    } catch {
      return false;
    }
  }
  return false;
}

/** 从工具参数里挑一个 url 字段（fetch 类工具）。 */
function pickUrlParam(params: Record<string, unknown>): string | undefined {
  for (const key of ["url", "endpoint", "uri", "href"]) {
    const v = params[key];
    if (typeof v === "string" && /^https?:\/\//.test(v)) return v;
  }
  return undefined;
}

type Candidate = { res: MatchResult; skillActive: boolean; strong: boolean };

function toResult(f: IndexedFn, matchType: MatchResult["matchType"], args: Record<string, unknown>): MatchResult {
  return {
    skillName: f.skillName,
    skillVersion: f.skillVersion,
    functionId: f.functionId,
    functionName: f.functionName,
    matchType,
    args,
  };
}

/**
 * 主匹配。命中返回 MatchResult；无自信命中返回 null。
 * 多候选时优先归属到 `activeSkills`（本 session 已触发的 skill）以消歧。
 */
export function match(
  call: ToolCall,
  activeSkills: ReadonlySet<string>,
  index: MatchIndex
): MatchResult | null {
  const candidates: Candidate[] = [];
  const add = (res: MatchResult, strong = true) =>
    candidates.push({ res, skillActive: activeSkills.has(res.skillName), strong });

  if (call.toolName === "exec") {
    const command = typeof call.params.command === "string" ? call.params.command : "";
    if (!command) return null;
    const tokens = tokenize(command);
    const flags = parseFlags(tokens);

    // script：按 token 的 basename 命中桶。
    //  - 强匹配：token 路径以配置脚本相对路径结尾（如完整 baseDir 路径）。
    //  - 弱匹配：仅 basename 相同（如 `cd` 进目录后 `python model_usage.py`）；
    //    弱匹配只有在「该 skill 已激活」时才会被采纳，避免跨 skill 同名脚本误判。
    for (const tok of tokens) {
      const fns = index.scriptByBasename.get(path.basename(tok));
      if (!fns) continue;
      for (const f of fns) {
        const rule = f.rule as ScriptMatchRule;
        if (!argRulesMatch(rule.argRules, flags)) continue;
        add(toResult(f, "script", flags), pathEndsWith(tok, rule.script));
      }
    }

    // command：命令头命中桶，再校验 targetPattern 出现在命令里（空白归一以增强鲁棒性）
    const head = commandHead(tokens);
    if (head) {
      const fns = index.commandByHead.get(head);
      if (fns) {
        const normCmd = command.replace(/\s+/g, " ");
        for (const f of fns) {
          const rule = f.rule as CommandMatchRule;
          if (normCmd.includes(rule.targetPattern.replace(/\s+/g, " "))) {
            add(toResult(f, "command", parseKeyValues(tokens)));
          }
        }
      }
    }

    // http：curl/wget 命令里的 URL
    if (index.httpRules.length > 0) {
      const url = extractUrl(command);
      if (url) {
        for (const f of index.httpRules) {
          if (httpMatchesUrl(url, f.rule as HttpMatchRule)) {
            add(toResult(f, "http", parseQuery(url)));
          }
        }
      }
    }
  } else {
    // 非 exec：tool 直调
    const exact = index.toolByName.get(call.toolName) ?? [];
    for (const f of exact) {
      const rule = f.rule as ToolMatchRule;
      if (whereMatches(rule.where, call.params)) add(toResult(f, "tool", { ...call.params }));
    }
    for (const f of index.toolFuzzy) {
      const rule = f.rule as ToolMatchRule;
      if (toolNameMatches(f, call.toolName) && whereMatches(rule.where, call.params)) {
        add(toResult(f, "tool", { ...call.params }));
      }
    }

    // http：fetch 类工具的 url 参数
    if (index.httpRules.length > 0) {
      const url = pickUrlParam(call.params);
      if (url) {
        for (const f of index.httpRules) {
          if (httpMatchesUrl(url, f.rule as HttpMatchRule)) {
            add(toResult(f, "http", parseQuery(url)));
          }
        }
      }
    }
  }

  if (candidates.length === 0) return null;
  // 丢弃「弱匹配且 skill 未激活」的低置信候选。
  const eligible = candidates.filter((c) => c.strong || c.skillActive);
  if (eligible.length === 0) return null;
  // 置信度排序：强且激活 > 强 > 弱且激活。sort 在 V8 稳定，平手时保留配置顺序。
  const rank = (c: Candidate) => (c.strong ? 2 : 0) + (c.skillActive ? 1 : 0);
  eligible.sort((a, b) => rank(b) - rank(a));
  return eligible[0].res;
}
