import { dirname, isAbsolute, join, posix, resolve } from "node:path";
import { fileURLToPath } from "node:url";

import { config } from "../config.js";
import {
  atomicWriteFileWithHistory,
  ensureDirectory,
  listFiles,
  readTextFile,
  removeFileIfExists,
  slugify,
  toStableId,
} from "../utils/fileStore.js";
import {
  sanitizePersistentScript,
  sanitizePersistentDocument,
  sanitizePersistentLine,
} from "../utils/persistencePolicy.js";

const LEARNED_PATTERNS_MARKER = "## Learned Patterns";
const ROOT_SKILL_ALIAS = "__root__";
const PROJECT_ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const resolveProjectPath = (path: string) =>
  isAbsolute(path) ? path : resolve(PROJECT_ROOT_DIR, path);
const DEFAULT_SKILLS_ROOT_DIR = resolveProjectPath(config.OPENCODE_SKILLS_ROOT_DIR);
const DEFAULT_SKILLS_BACKUP_DIR = resolveProjectPath(
  join(config.PERSISTENCE_BACKUP_DIR, "skills"),
);

export type SkillPatternRecord = {
  id: string;
  content: string;
};

export class SkillStore {
  private writeQueue: Promise<void> = Promise.resolve();

  constructor(
    private readonly rootDir = DEFAULT_SKILLS_ROOT_DIR,
    private readonly backupDir = DEFAULT_SKILLS_BACKUP_DIR,
  ) {}

  async list(skillPath: string) {
    const normalizedSkillPath = normalizeSkillPath(skillPath);
    if (!normalizedSkillPath) {
      return null;
    }
    const target = this.skillFilePath(normalizedSkillPath);
    const current =
      (await readTextFile(target)) ?? defaultSkillDocument(normalizedSkillPath);
    return {
      references: await this.listReferenceFiles(normalizedSkillPath),
      scripts: await this.listScriptFiles(normalizedSkillPath),
      skillPath: normalizedSkillPath,
      target,
      patterns: extractLearnedPatterns(current),
    };
  }

  async appendPattern(skillPath: string, pattern: string) {
    const normalizedSkillPath = normalizeSkillPath(skillPath);
    if (!normalizedSkillPath) {
      return { changed: false, detail: "invalid skill_path", target: "" };
    }
    const sanitizedPattern = sanitizePersistentLine(pattern, 320);
    if (!sanitizedPattern) {
      return { changed: false, detail: "pattern rejected by persistence policy", target: "" };
    }
    return this.serializeWrite(async () => {
      const target = this.skillFilePath(normalizedSkillPath);
      const current =
        (await readTextFile(target)) ?? defaultSkillDocument(normalizedSkillPath);
      const existingPatterns = extractLearnedPatterns(current);
      if (existingPatterns.some((entry) => entry.content === sanitizedPattern)) {
        return { changed: false, detail: "pattern already existed", target };
      }
      const record: SkillPatternRecord = {
        content: sanitizedPattern,
        id: toStableId(normalizedSkillPath, sanitizedPattern.toLowerCase()),
      };
      const next = current.includes(LEARNED_PATTERNS_MARKER)
        ? current.replace(
            LEARNED_PATTERNS_MARKER,
            `${LEARNED_PATTERNS_MARKER}\n- [${record.id}] ${record.content}`,
          )
        : `${current.trimEnd()}\n\n${LEARNED_PATTERNS_MARKER}\n- [${record.id}] ${record.content}\n`;
      await ensureDirectory(join(this.rootDir, normalizedSkillPath));
      await atomicWriteFileWithHistory(target, next, {
        backupDir: this.backupDir,
        rootDir: this.rootDir,
      });
      return { changed: true, detail: "skill file updated", target };
    });
  }

  async writeSkill(skillPath: string, content: string) {
    const normalizedSkillPath = normalizeSkillPath(skillPath);
    if (!normalizedSkillPath) {
      return { changed: false, detail: "invalid skill_path", target: "" };
    }
    const sanitizedContent = sanitizePersistentDocument(content, 12000);
    if (!sanitizedContent) {
      return { changed: false, detail: "skill content rejected by persistence policy", target: "" };
    }
    if (!hasValidSkillFrontmatter(sanitizedContent)) {
      return {
        changed: false,
        detail: "skill content rejected: expected SKILL.md frontmatter with name and description",
        target: "",
      };
    }
    return this.serializeWrite(async () => {
      const target = this.skillFilePath(normalizedSkillPath);
      await ensureDirectory(this.skillDirPath(normalizedSkillPath));
      await atomicWriteFileWithHistory(target, `${sanitizedContent}\n`, {
        backupDir: this.backupDir,
        rootDir: this.rootDir,
      });
      return { changed: true, detail: "skill written", target };
    });
  }

  async removeSkill(skillPath: string) {
    const normalizedSkillPath = normalizeSkillPath(skillPath);
    if (!normalizedSkillPath) {
      return { changed: false, detail: "invalid skill_path", target: "" };
    }
    return this.serializeWrite(async () => {
      const target = this.skillFilePath(normalizedSkillPath);
      const previous = await readTextFile(target);
      if (previous === null) {
        return { changed: false, detail: "skill file not found", target };
      }
      await removeFileIfExists(target);
      return { changed: true, detail: "skill removed", target };
    });
  }

  async removePattern(skillPath: string, targetId: string) {
    const normalizedSkillPath = normalizeSkillPath(skillPath);
    if (!normalizedSkillPath) {
      return { changed: false, detail: "invalid skill_path", target: "" };
    }
    return this.serializeWrite(async () => {
      const target = this.skillFilePath(normalizedSkillPath);
      const current = await readTextFile(target);
      if (!current) {
        return { changed: false, detail: "skill file not found", target };
      }
      const patterns = extractLearnedPatterns(current);
      const remaining = patterns.filter((entry) => entry.id !== targetId.trim());
      if (remaining.length === patterns.length) {
        return { changed: false, detail: "pattern not found", target };
      }
      const next = rewriteLearnedPatterns(current, remaining);
      await atomicWriteFileWithHistory(target, next, {
        backupDir: this.backupDir,
        rootDir: this.rootDir,
      });
      return { changed: true, detail: "pattern removed", target };
    });
  }

  async writeReference(skillPath: string, filePath: string, content: string) {
    const normalizedSkillPath = normalizeSkillPath(skillPath);
    const normalizedReferencePath = normalizeReferencePath(filePath);
    if (!normalizedSkillPath) {
      return { changed: false, detail: "invalid skill_path", target: "" };
    }
    if (!normalizedReferencePath) {
      return { changed: false, detail: "invalid reference file_path", target: "" };
    }
    const sanitizedContent = sanitizePersistentDocument(content, 5000);
    if (!sanitizedContent) {
      return { changed: false, detail: "reference content rejected by persistence policy", target: "" };
    }
    return this.serializeWrite(async () => {
      const target = join(this.rootDir, normalizedSkillPath, normalizedReferencePath);
      await ensureDirectory(dirname(target));
      await atomicWriteFileWithHistory(target, `${sanitizedContent}\n`, {
        backupDir: this.backupDir,
        rootDir: this.rootDir,
      });
      return { changed: true, detail: "reference written", target };
    });
  }

  async removeReference(skillPath: string, filePath: string) {
    const normalizedSkillPath = normalizeSkillPath(skillPath);
    const normalizedReferencePath = normalizeReferencePath(filePath);
    if (!normalizedSkillPath) {
      return { changed: false, detail: "invalid skill_path", target: "" };
    }
    if (!normalizedReferencePath) {
      return { changed: false, detail: "invalid reference file_path", target: "" };
    }
    return this.serializeWrite(async () => {
      const target = join(this.rootDir, normalizedSkillPath, normalizedReferencePath);
      const previous = await readTextFile(target);
      if (previous === null) {
        return { changed: false, detail: "reference not found", target };
      }
      await removeFileIfExists(target);
      return { changed: true, detail: "reference removed", target };
    });
  }

  async writeScript(skillPath: string, filePath: string, content: string) {
    const normalizedSkillPath = normalizeSkillPath(skillPath);
    const normalizedScriptPath = normalizeScriptPath(filePath);
    if (!normalizedSkillPath) {
      return { changed: false, detail: "invalid skill_path", target: "" };
    }
    if (!normalizedScriptPath) {
      return { changed: false, detail: "invalid script file_path", target: "" };
    }
    const sanitizedContent = sanitizePersistentScript(content, 20000);
    if (!sanitizedContent) {
      return { changed: false, detail: "script content rejected by persistence policy", target: "" };
    }
    return this.serializeWrite(async () => {
      const target = join(this.rootDir, normalizedSkillPath, normalizedScriptPath);
      await ensureDirectory(dirname(target));
      await atomicWriteFileWithHistory(target, sanitizedContent, {
        backupDir: this.backupDir,
        rootDir: this.rootDir,
      });
      return { changed: true, detail: "script written", target };
    });
  }

  async removeScript(skillPath: string, filePath: string) {
    const normalizedSkillPath = normalizeSkillPath(skillPath);
    const normalizedScriptPath = normalizeScriptPath(filePath);
    if (!normalizedSkillPath) {
      return { changed: false, detail: "invalid skill_path", target: "" };
    }
    if (!normalizedScriptPath) {
      return { changed: false, detail: "invalid script file_path", target: "" };
    }
    return this.serializeWrite(async () => {
      const target = join(this.rootDir, normalizedSkillPath, normalizedScriptPath);
      const previous = await readTextFile(target);
      if (previous === null) {
        return { changed: false, detail: "script not found", target };
      }
      await removeFileIfExists(target);
      return { changed: true, detail: "script removed", target };
    });
  }

  private async listReferenceFiles(skillPath: string) {
    const referenceDir = join(this.rootDir, skillPath, "references");
    if (skillPath === ROOT_SKILL_ALIAS) {
      return [];
    }
    const files = await listFiles(referenceDir);
    return files.map((file) => file.slice(referenceDir.length + 1));
  }

  private async listScriptFiles(skillPath: string) {
    if (skillPath === ROOT_SKILL_ALIAS) {
      return [];
    }
    const scriptDir = join(this.rootDir, skillPath, "scripts");
    const files = await listFiles(scriptDir);
    return files.map((file) => file.slice(scriptDir.length + 1));
  }

  private skillFilePath(skillPath: string) {
    return join(this.skillDirPath(skillPath), "SKILL.md");
  }

  private skillDirPath(skillPath: string) {
    return skillPath === ROOT_SKILL_ALIAS ? this.rootDir : join(this.rootDir, skillPath);
  }

  private async serializeWrite<T>(task: () => Promise<T>) {
    const run = this.writeQueue.catch(() => undefined).then(task);
    this.writeQueue = run.then(
      () => undefined,
      () => undefined,
    );
    return run;
  }
}

export const normalizeSkillPath = (rawSkillPath: string) => {
  const normalized = posix.normalize(rawSkillPath.trim().replace(/^\/+|\/+$/g, ""));
  if (normalized === ROOT_SKILL_ALIAS) {
    return ROOT_SKILL_ALIAS;
  }
  if (!normalized || normalized === "." || normalized.startsWith("..")) {
    return null;
  }
  if (normalized === "SKILL.md" || normalized.endsWith("/SKILL.md")) {
    return null;
  }
  if (!/^[a-z0-9._/-]+$/i.test(normalized)) {
    return null;
  }
  return normalized;
};

const normalizeReferencePath = (rawFilePath: string) => {
  const normalized = posix.normalize(rawFilePath.trim().replace(/^\/+|\/+$/g, ""));
  if (!normalized || normalized.startsWith("..")) {
    return null;
  }
  if (!normalized.startsWith("references/")) {
    return null;
  }
  if (!normalized.endsWith(".md")) {
    return null;
  }
  const segments = normalized.split("/");
  const last = segments.pop();
  if (!last) {
    return null;
  }
  const stem = last.replace(/\.md$/i, "");
  const normalizedStem = slugify(stem);
  return [...segments, `${normalizedStem}.md`].join("/");
};

const normalizeScriptPath = (rawFilePath: string) => {
  const normalized = posix.normalize(rawFilePath.trim().replace(/^\/+|\/+$/g, ""));
  if (!normalized || normalized.startsWith("..")) {
    return null;
  }
  if (!normalized.startsWith("scripts/")) {
    return null;
  }
  if (!normalized.endsWith(".py")) {
    return null;
  }
  const segments = normalized.split("/");
  const last = segments.pop();
  if (!last) {
    return null;
  }
  const stem = last.replace(/\.py$/i, "");
  const normalizedStem = slugify(stem);
  return [...segments, `${normalizedStem}.py`].join("/");
};

export const extractLearnedPatterns = (content: string): SkillPatternRecord[] => {
  const section = extractLearnedPatternsSection(content);
  if (!section) {
    return [];
  }
  return section
    .split("\n")
    .map((line) => line.trim())
    .filter((line) => line.startsWith("- "))
    .map((line) => line.slice(2).trim())
    .map((line) => {
      const idMatch = line.match(/^\[([a-z0-9]{8,})\]\s+(.*)$/i);
      if (idMatch) {
        return {
          content: idMatch[2],
          id: idMatch[1],
        };
      }
      return {
        content: line,
        id: toStableId("skill-pattern", line.toLowerCase()),
      };
    })
    .filter((entry) => entry.content);
};

const rewriteLearnedPatterns = (content: string, patterns: SkillPatternRecord[]) => {
  const renderedSection =
    patterns.length > 0
      ? `${LEARNED_PATTERNS_MARKER}\n${patterns.map((entry) => `- [${entry.id}] ${entry.content}`).join("\n")}`
      : `${LEARNED_PATTERNS_MARKER}\n`;
  if (!content.includes(LEARNED_PATTERNS_MARKER)) {
    return `${content.trimEnd()}\n\n${renderedSection}\n`;
  }
  const markerIndex = content.indexOf(LEARNED_PATTERNS_MARKER);
  const afterMarkerIndex = markerIndex + LEARNED_PATTERNS_MARKER.length;
  const tail = content.slice(afterMarkerIndex);
  const nextHeadingMatch = tail.match(/\n##\s+/);
  const sectionEndOffset = nextHeadingMatch?.index ?? tail.length;
  const head = content.slice(0, markerIndex).trimEnd();
  const suffix = tail.slice(sectionEndOffset).trimStart();
  return suffix
    ? `${head}\n\n${renderedSection}\n\n${suffix}`
    : `${head}\n\n${renderedSection}\n`;
};

const extractLearnedPatternsSection = (content: string) => {
  const markerIndex = content.indexOf(LEARNED_PATTERNS_MARKER);
  if (markerIndex === -1) {
    return "";
  }
  const tail = content.slice(markerIndex + LEARNED_PATTERNS_MARKER.length);
  const nextHeadingMatch = tail.match(/\n##\s+/);
  return tail.slice(0, nextHeadingMatch?.index ?? tail.length);
};

const hasValidSkillFrontmatter = (content: string) => {
  const match = content.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
  if (!match) {
    return false;
  }
  const lines = match[1].split("\n").map((line) => line.trim());
  return (
    lines.some((line) => /^name:\s*\S+/i.test(line)) &&
    lines.some((line) => /^description:\s*\S+/i.test(line))
  );
};

const defaultRootSkill = () => `---
name: skills
description: TJWater Skills root index.
---

# TJWater Skills
`;

const defaultLearnedSkill = (skillPath: string) => `---
name: tjwater-action-${skillPath
  .split("/")
  .filter(Boolean)
  .join("-")
  .replace(/[^a-z0-9._-]+/gi, "-")
  .replace(/^-+|-+$/g, "")
  .slice(0, 120) || "generated-skill"}
description: 由 skill_manager 在线维护的高置信度可复用 workflow。
version: 1.0.0
---

# learned skill

## 简介

记录由 \`skill_manager\` 在线维护的高置信度 workflow 模式。

## Learned Patterns
`;

const defaultSkillDocument = (skillPath: string) =>
  skillPath === ROOT_SKILL_ALIAS ? defaultRootSkill() : defaultLearnedSkill(skillPath);
