/**
 * 智能动态拉取 skill 标准配置。
 *
 * - 扫描本地已装 skill（extensions 下的 SKILL.md），算签名（版本或文件 mtime/size）。
 * - 与本地缓存的同步状态比对，找出「新增 / 签名变化」的 skill，按需拉取标准配置。
 * - 拉取来源：配了 platformBaseUrl → POST 平台接口；否则用本地静态桩（第一步交付）。
 * - 维护 matcher 索引：任何配置变化后重建。
 *
 * 触发时机：① skill_trigger 懒触发(lazyCheck) ② 3 分钟周期 reconcile ③ before_install 后 reconcile。
 * 全部异常吞掉，绝不阻塞 hook。
 */
import fs from "node:fs/promises";
import fsSync from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { PluginConfig, SkillStandardConfig } from "./types.ts";
import { type PluginPaths, resolveAgentSkillDirs, openclawHome } from "./paths.ts";
import { buildIndex, emptyIndex, type MatchIndex } from "./matcher.ts";
import { defaultFetch } from "./http.ts";
import { isOutdated } from "./semver.ts";
import { parseSkillVersion, readSkillVersion } from "./skill-version.ts";
import type { OutdatedCopy } from "./updater.ts";

// 保持对外导出位置不变（历史测试从 config-sync 导入）。
export { parseSkillVersion };

type FetchLike = (url: string, init: RequestInit) => Promise<{ ok: boolean; status: number; json: () => Promise<unknown> }>;

/** 扫描得到的一个已装 skill。 */
export type InstalledSkill = {
  name: string;
  version?: string;
  rootDir: string;
  signature: string;
};

/** 持久化的同步状态：每个 skill 的签名 + 多版本配置缓存池。 */
type SyncState = {
  // 旧版本兼容字段
  skills?: Record<string, { signature: string; version?: string; config?: SkillStandardConfig }>;
  // 新版本字段
  active?: Record<string, { signature: string }>;
  configPool?: Record<string, SkillStandardConfig>;
  /** agent↔skill 安装映射：skill 名 → 各 workspace 下的副本（目录 + 本地版本）。本地留存，便于排查与更新定位。 */
  installations?: Record<string, Array<{ rootDir: string; version?: string }>>;
};

export type ConfigSyncOptions = {
  paths: Pick<PluginPaths, "extensionsDir" | "syncStatePath" | "openclawConfigPath">;
  getConfig: () => PluginConfig;
  fetchImpl?: FetchLike;
  /** 注入静态桩配置，便于测试；默认从 sample-config.json 读。 */
  sampleConfigs?: SkillStandardConfig[];
  /**
   * 动态解析 skill 扫描目录（每次扫描调用，因此运行期新增 agent workspace 无需重启即可被发现）。
   * 默认按 openclawConfigPath 重读 openclaw.json 解析。测试可注入固定目录。
   */
  resolveSkillDirs?: () => string[];
  /** 版本更新器（可选）。注入后，reconcile 检测到落后副本时触发自动更新（受 autoUpdateSkills 开关约束）。 */
  updater?: { applyUpdates(outdated: OutdatedCopy[]): Promise<void> };
};

const MAX_SCAN_DEPTH = 6;
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", ".cache"]);
/** lazyCheck 扫描缓存有效期：足够吸收同一会话内的连续 skill_trigger，又不至于明显滞后真实安装变化。 */
const SCAN_CACHE_TTL_MS = 5000;

export class ConfigSync {
  private readonly paths: Pick<PluginPaths, "extensionsDir" | "syncStatePath" | "openclawConfigPath">;
  private readonly getConfig: () => PluginConfig;
  private readonly fetchImpl: FetchLike;
  private sampleConfigs?: SkillStandardConfig[];
  private readonly resolveSkillDirs: () => string[];
  private readonly updater?: { applyUpdates(outdated: OutdatedCopy[]): Promise<void> };

  private signatures = new Map<string, string>();
  private configs = new Map<string, SkillStandardConfig>();
  private configPool = new Map<string, SkillStandardConfig>();
  private index: MatchIndex = emptyIndex();
  /** skill 根目录 → 规范名（SKILL.md frontmatter name）。用于把触发事件归一到与匹配一致的身份。 */
  private skillNameByDir = new Map<string, string>();
  /** 同一时刻只跑一次 reconcile，避免周期/懒触发并发。 */
  private reconciling = false;
  /** 同一时刻只跑一次版本检查，避免周期/启动并发。 */
  private checkingVersions = false;
  /** skill 名 → 平台最新版本（由 30 分钟版本检查刷新；缺省回退用已装配置的 version）。 */
  private latestVersions = new Map<string, string>();
  /** scanInstalledSkills 的短 TTL 缓存，仅服务 lazyCheck 的高频触发；reconcile 始终全新扫描并刷新它。 */
  private scanCache?: { ts: number; skills: InstalledSkill[] };
  /** skill 名 → 所有安装副本（多 agent workspace 各一份）。每次扫描刷新。 */
  private installations = new Map<string, InstalledSkill[]>();

  constructor(opts: ConfigSyncOptions) {
    this.paths = opts.paths;
    this.getConfig = opts.getConfig;
    this.fetchImpl = opts.fetchImpl ?? defaultFetch();
    this.sampleConfigs = opts.sampleConfigs;
    // 默认：每次扫描重读 openclaw.json，动态发现新增 agent workspace（无需重启网关）。
    this.resolveSkillDirs =
      opts.resolveSkillDirs ?? (() => resolveAgentSkillDirs(openclawHome(), this.paths.openclawConfigPath));
    this.updater = opts.updater;
  }

  private get isDebug(): boolean {
    return this.getConfig().debugLogging !== false;
  }

  private debug(...args: any[]): void {
    if (this.isDebug) {
      console.log("[skill-logger-plugin/config-sync]", ...args);
    }
  }

  getIndex(): MatchIndex {
    return this.index;
  }

  getVersion(skillName: string): string | undefined {
    const cfg = this.configs.get(skillName);
    if (cfg?.version) return cfg.version;
    const sig = this.signatures.get(skillName);
    if (sig) {
      const v = sig.split("|")[0];
      return v ? v : undefined;
    }
    return undefined;
  }

  /** 把 SKILL.md 所在目录解析为规范 skill 名；未扫描到时返回 undefined（调用方回退目录名）。 */
  resolveSkillName(rootDir: string): string | undefined {
    return this.skillNameByDir.get(rootDir);
  }

  /** 从磁盘加载已缓存的同步状态并重建索引（gateway 启动时调一次）。 */
  async load(): Promise<void> {
    try {
      const raw = await fs.readFile(this.paths.syncStatePath, "utf-8");
      const state = JSON.parse(raw) as SyncState;
      // 兼容旧版本格式
      if (state.skills) {
        for (const [name, entry] of Object.entries(state.skills)) {
          this.signatures.set(name, entry.signature);
          if (entry.config) {
            this.configs.set(name, entry.config);
            const cacheKey = `${name}@${entry.version || "unknown"}`;
            this.configPool.set(cacheKey, entry.config);
          }
        }
      }
      // 新版本格式
      if (state.configPool) {
        for (const [key, cfg] of Object.entries(state.configPool)) {
          this.configPool.set(key, cfg);
        }
      }
      if (state.active) {
        for (const [name, entry] of Object.entries(state.active)) {
          this.signatures.set(name, entry.signature);
          const v = entry.signature.split("|")[0];
          const cacheKey = `${name}@${v || "unknown"}`;
          const cfg = this.configPool.get(cacheKey);
          if (cfg) this.configs.set(name, cfg);
        }
      }
      this.rebuildIndex();
    } catch {
      // 无状态文件，留空
    }
  }

  /**
   * 递归扫描 extensions 及各 agent workspace 下所有 SKILL.md，解析名称/版本/签名。
   * 同名 skill 可能分布在多个 workspace（如 coder/coder2 各持一份副本）。缓存以 skill 名为全局 key，
   * 故这里按名去重并取确定性的一份（按 rootDir 排序后取首个），避免 reconcile 每轮在不同副本的
   * 签名间反复横跳、触发无意义的重复拉取与持久化抖动。skillNameByDir 仍保留全部副本目录的映射。
   */
  async scanInstalledSkills(): Promise<InstalledSkill[]> {
    const out: InstalledSkill[] = [];
    const walk = async (dir: string, depth: number): Promise<void> => {
      if (depth > MAX_SCAN_DEPTH) return;
      let entries: fsSync.Dirent[];
      try {
        entries = await fs.readdir(dir, { withFileTypes: true });
      } catch {
        return;
      }
      for (const e of entries) {
        if (e.isDirectory()) {
          if (SKIP_DIRS.has(e.name)) continue;
          await walk(path.join(dir, e.name), depth + 1);
        } else if (e.name === "SKILL.md") {
          const skillMd = path.join(dir, e.name);
          const skill = await this.readSkill(dir, skillMd);
          if (skill) {
            out.push(skill);
            this.skillNameByDir.set(skill.rootDir, skill.name);
          }
        }
      }
    };
    await walk(this.paths.extensionsDir, 0);
    // 动态解析所有 agent workspace 的 skills 目录（含顶层全局 skills）：每次扫描重读 openclaw.json，
    // 运行期新增 agent 无需重启即可被发现。不存在的目录在 walk 内已被吞掉。
    for (const skillsDir of this.resolveSkillDirs()) {
      await walk(skillsDir, 0);
    }
    out.sort((a, b) => a.rootDir.localeCompare(b.rootDir));
    // 记录 skill → 所有安装副本（含各 workspace 下的 rootDir 与本地版本），供版本更新定位覆盖目标。
    const installs = new Map<string, InstalledSkill[]>();
    for (const s of out) {
      const arr = installs.get(s.name);
      if (arr) arr.push(s);
      else installs.set(s.name, [s]);
    }
    this.installations = installs;
    // 按 skill 名去重，取确定性的一份（rootDir 字典序最小）。
    const deduped = new Map<string, InstalledSkill>();
    for (const s of out) {
      if (!deduped.has(s.name)) deduped.set(s.name, s);
    }
    return [...deduped.values()];
  }

  /** skill → 所有安装副本（多 agent workspace 各一份）。供版本更新定位需要覆盖的目录。 */
  getInstallations(): ReadonlyMap<string, ReadonlyArray<InstalledSkill>> {
    return this.installations;
  }

  /** 基于最近一次版本检查结果，找出所有本地版本落后的安装副本。 */
  detectOutdated(): OutdatedCopy[] {
    const out: OutdatedCopy[] = [];
    for (const [skillName, copies] of this.installations) {
      const cfg = this.configs.get(skillName);
      const latestVersion = this.latestVersions.get(skillName) || cfg?.latestVersion || cfg?.version;
      if (!latestVersion) continue;
      for (const copy of copies) {
        if (!isOutdated(copy.version, latestVersion)) continue;
        out.push({
          skillName,
          rootDir: copy.rootDir,
          localVersion: copy.version || "",
          latestVersion,
        });
      }
    }
    return out;
  }

  /** 扫描本地安装副本，拉取平台最新版本，检测落后副本并按配置触发自动更新。 */
  async checkVersionsAndUpdate(): Promise<void> {
    if (this.checkingVersions) return;
    this.checkingVersions = true;
    try {
      const installed = await this.scanInstalledSkills();
      this.scanCache = { ts: Date.now(), skills: installed };

      if (installed.length > 0) {
        const { ok, configs } = await this.pullConfigs(
          installed.map((s) => ({ name: s.name, version: s.version }))
        );
        if (ok) {
          for (const cfg of configs) {
            const latestVersion = cfg.latestVersion || cfg.version;
            if (latestVersion) this.latestVersions.set(cfg.skillName, latestVersion);
            if (cfg.version) {
              this.configPool.set(`${cfg.skillName}@${cfg.version}`, cfg);
              if (!this.configs.has(cfg.skillName)) this.configs.set(cfg.skillName, cfg);
            }
          }
        }
      }

      const outdated = this.detectOutdated();
      if (outdated.length > 0 && this.updater) {
        await this.updater.applyUpdates(outdated);
        const refreshed = await this.scanInstalledSkills();
        this.scanCache = { ts: Date.now(), skills: refreshed };
      }

      await this.persist();
    } catch (err) {
      console.warn("[skill-logger-plugin] checkVersionsAndUpdate 异常", err);
    } finally {
      this.checkingVersions = false;
    }
  }

  /** lazyCheck 专用：命中短 TTL 缓存则跳过整树遍历；reconcile 不走此路径，始终全新扫描。 */
  private async scanInstalledSkillsCached(): Promise<InstalledSkill[]> {
    const now = Date.now();
    if (this.scanCache && now - this.scanCache.ts < SCAN_CACHE_TTL_MS) {
      return this.scanCache.skills;
    }
    const skills = await this.scanInstalledSkills();
    this.scanCache = { ts: now, skills };
    return skills;
  }

  private async readSkill(rootDir: string, skillMdPath: string): Promise<InstalledSkill | undefined> {
    try {
      const content = await fs.readFile(skillMdPath, "utf-8");
      const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content)?.[1] ?? "";
      const name = /(^|\n)name:\s*(.+)/.exec(fm)?.[2]?.trim() || path.basename(rootDir);
      const version = await readSkillVersion(rootDir, content);
      const signature = await this.computeSignature(rootDir, skillMdPath, version);
      return { name, version, rootDir, signature };
    } catch {
      return undefined;
    }
  }

  /** 签名 = 版本（若有）+ SKILL.md 与 scripts/ 的 mtime/size 摘要。 */
  private async computeSignature(rootDir: string, skillMdPath: string, version?: string): Promise<string> {
    const parts: string[] = [version ?? ""];
    try {
      const st = await fs.stat(skillMdPath);
      parts.push(`md:${st.mtimeMs}:${st.size}`);
    } catch {
      /* ignore */
    }
    try {
      const scriptsDir = path.join(rootDir, "scripts");
      const entries = await fs.readdir(scriptsDir, { withFileTypes: true });
      for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
        if (!e.isFile()) continue;
        const st = await fs.stat(path.join(scriptsDir, e.name));
        parts.push(`s:${e.name}:${st.mtimeMs}:${st.size}`);
      }
    } catch {
      /* 无 scripts 目录 */
    }
    return parts.join("|");
  }

  /** 找出需要拉取（新增或签名变化）与已移除的 skill。优先命中本地 configPool。 */
  diffAgainstState(installed: InstalledSkill[]): { toFetch: InstalledSkill[]; removed: string[]; cached: InstalledSkill[] } {
    const toFetch: InstalledSkill[] = [];
    const cached: InstalledSkill[] = [];
    const removed: string[] = [];
    const seen = new Set<string>();
    for (const s of installed) {
      seen.add(s.name);
      if (this.signatures.get(s.name) !== s.signature) {
        const cacheKey = `${s.name}@${s.version || "unknown"}`;
        if (this.configPool.has(cacheKey)) {
          cached.push(s);
        } else {
          toFetch.push(s);
        }
      }
    }
    for (const name of this.signatures.keys()) {
      if (!seen.has(name)) removed.push(name);
    }
    return { toFetch, removed, cached };
  }

  /** 全量对账：扫描 → diff → 拉取 → 更新缓存 → 重建索引 → 持久化。 */
  async reconcile(): Promise<void> {
    if (this.reconciling) return;
    this.reconciling = true;
    try {
      const installed = await this.scanInstalledSkills();
      this.scanCache = { ts: Date.now(), skills: installed }; // 刷新 lazyCheck 缓存，保证安装后对账的新鲜度
      this.debug(`reconcile: Found ${installed.length} installed skills.`);
      const { toFetch, removed, cached } = this.diffAgainstState(installed);
      if (toFetch.length === 0 && removed.length === 0 && cached.length === 0) return;

      // 命中本地池的，直接置为活跃
      for (const s of cached) {
        this.signatures.set(s.name, s.signature);
        this.configs.set(s.name, this.configPool.get(`${s.name}@${s.version || "unknown"}`)!);
        this.debug(`reconcile: Skill ${s.name}@${s.version} instantly loaded from local configPool.`);
      }

      if (toFetch.length > 0) {
        this.debug(`reconcile: Fetching configs for ${toFetch.length} skills...`);
        const { ok, configs } = await this.pullConfigs(
          toFetch.map((s) => ({ name: s.name, version: s.version }))
        );
        // 仅在拿到确定性结果时推进签名；拉取失败则不推进，下轮重试。
        if (ok) {
          const byName = new Map(configs.map((c) => [c.skillName, c]));
          this.debug(`reconcile: Fetched ${configs.length} configs successfully.`);
          for (const s of toFetch) {
            const cfg = byName.get(s.name) as SkillStandardConfig & { status?: string };
            
            // If the platform says this config is still being processed or reviewed,
            // we skip updating the signature so it will be retried in the next reconcile.
            if (cfg && (cfg.status === 'REVIEW_NEEDED' || cfg.status === 'EXTRACTING')) {
              this.debug(`reconcile: Skill ${s.name} is ${cfg.status}, skipping signature update.`);
              continue;
            }

            this.signatures.set(s.name, s.signature);
            if (cfg) {
              this.configs.set(s.name, cfg);
              this.configPool.set(`${s.name}@${s.version || "unknown"}`, cfg);
            } else {
              this.configs.delete(s.name); // 平台明确无此配置
            }
          }
        }
      }
      for (const name of removed) {
        this.signatures.delete(name);
        this.configs.delete(name);
      }
      this.rebuildIndex();
      await this.persist();
    } catch (err) {
      console.warn("[skill-logger-plugin] reconcile 异常", err);
    } finally {
      this.reconciling = false;
    }
  }

  /** 懒触发：仅检查某个被触发的 skill，缺配置/签名变才拉。`ident` 可为规范名或目录名。 */
  async lazyCheck(ident: string): Promise<void> {
    try {
      const find = (list: InstalledSkill[]) =>
        list.find((x) => x.name === ident || path.basename(x.rootDir) === ident);
      let s = find(await this.scanInstalledSkillsCached());
      // 缓存里没有该 skill：可能是刚安装的新 skill，强制全新扫描兜底，行为与未加缓存前一致。
      if (!s) {
        const fresh = await this.scanInstalledSkills();
        this.scanCache = { ts: Date.now(), skills: fresh };
        s = find(fresh);
      }
      if (!s) return;
      if (this.signatures.get(s.name) === s.signature && this.configs.has(s.name)) return;
      
      const cacheKey = `${s.name}@${s.version || "unknown"}`;
      if (this.configPool.has(cacheKey)) {
        this.debug(`lazyCheck: Skill ${s.name}@${s.version} loaded instantly from local configPool.`);
        this.signatures.set(s.name, s.signature);
        this.configs.set(s.name, this.configPool.get(cacheKey)!);
        this.rebuildIndex();
        await this.persist();
        return;
      }

      this.debug(`lazyCheck: Fetching config for ${s.name}@${s.version}...`);
      const { ok, configs } = await this.pullConfigs([{ name: s.name, version: s.version }]);
      if (!ok) return; // 拉取失败，保留旧状态，下轮重试
      
      this.debug(`lazyCheck: Fetched ${configs.length} configs successfully.`);
      const cfg = configs.find((c) => c.skillName === s.name) as SkillStandardConfig & { status?: string };
      
      if (cfg && (cfg.status === 'REVIEW_NEEDED' || cfg.status === 'EXTRACTING')) {
        this.debug(`lazyCheck: Skill ${s.name} is ${cfg.status}, skipping signature update.`);
        return;
      }

      this.signatures.set(s.name, s.signature);
      if (cfg) {
        this.configs.set(s.name, cfg);
        this.configPool.set(cacheKey, cfg);
      } else {
        this.configs.delete(s.name);
      }
      this.rebuildIndex();
      await this.persist();
    } catch (err) {
      console.warn("[skill-logger-plugin] lazyCheck 异常", err);
    }
  }

  /**
   * 拉取标准配置。配了 platformBaseUrl → POST 平台；否则用本地静态桩。
   *
   * 返回 `ok` 区分「确定性结果」与「拉取失败」：
   *  - ok=true  ：拿到了平台的明确答复（configs 可能为空，表示平台对这些 skill 暂无配置）。
   *  - ok=false ：网络/服务异常，调用方**不应**推进签名，下轮重试。
   */
  async pullConfigs(
    skillRefs: { name: string; version?: string }[]
  ): Promise<{ ok: boolean; configs: SkillStandardConfig[] }> {
    const config = this.getConfig();
    if (config.platformBaseUrl) {
      try {
        const url = config.platformBaseUrl.replace(/\/$/, "") + "/skill_config/pull";
        const headers: Record<string, string> = { "Content-Type": "application/json" };
        if (config.authToken) headers.Authorization = config.authToken;
        const res = await this.fetchImpl(url, {
          method: "POST",
          headers,
          body: JSON.stringify({ skills: skillRefs }),
        });
        if (!res.ok) {
          console.warn("[skill-logger-plugin] 拉取标准配置失败，HTTP", res.status);
          return { ok: false, configs: [] };
        }
        const data = (await res.json()) as { configs?: SkillStandardConfig[] };
        return { ok: true, configs: data.configs ?? [] };
      } catch (err) {
        console.warn("[skill-logger-plugin] 拉取标准配置异常", err);
        return { ok: false, configs: [] };
      }
    }
    // 本地静态桩：仅返回请求到的 skill（视为确定性结果）
    const want = new Set(skillRefs.map((r) => r.name));
    return { ok: true, configs: (await this.loadSampleConfigs()).filter((c) => want.has(c.skillName)) };
  }

  private async loadSampleConfigs(): Promise<SkillStandardConfig[]> {
    if (this.sampleConfigs) return this.sampleConfigs;
    try {
      const here = path.dirname(fileURLToPath(import.meta.url));
      const raw = await fs.readFile(path.join(here, "sample-config.json"), "utf-8");
      this.sampleConfigs = (JSON.parse(raw) as { configs: SkillStandardConfig[] }).configs;
    } catch {
      this.sampleConfigs = [];
    }
    return this.sampleConfigs;
  }

  private rebuildIndex(): void {
    this.index = buildIndex([...this.configs.values()]);
  }

  private async persist(): Promise<void> {
    try {
      const state: SyncState = { active: {}, configPool: {}, installations: {} };
      for (const [name, signature] of this.signatures) {
        state.active![name] = { signature };
      }
      for (const [key, cfg] of this.configPool) {
        state.configPool![key] = cfg;
      }
      // agent↔skill 安装映射：本地留存每个 skill 在各 workspace 的副本与版本。
      for (const [name, copies] of this.installations) {
        state.installations![name] = copies.map((c) => ({ rootDir: c.rootDir, version: c.version }));
      }
      await fs.mkdir(path.dirname(this.paths.syncStatePath), { recursive: true });
      // 原子写：写临时文件再 rename，避免 reconcile 与版本检查并发持久化时相互写坏。
      const tmp = `${this.paths.syncStatePath}.tmp-${process.pid}-${Date.now()}`;
      await fs.writeFile(tmp, JSON.stringify(state));
      await fs.rename(tmp, this.paths.syncStatePath);
    } catch (err) {
      console.warn("[skill-logger-plugin] 持久化同步状态失败", err);
    }
  }
}
