import { cp, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import type { RuntimeManager } from "@qjclaw/runtime-manager";
import type { ProjectStoreService } from "./project-store.js";
import type { SkillExecutionTarget, SkillStoreService } from "./skill-store.js";

interface PreparedSkillExecution {
  prompt: string;
  skillName: string;
  runtimeSkillName: string;
  runtimeSkillDir?: string;
  localPath: string;
}

interface BundledRuntimeManifestShape {
  packagedOpenClawEntry?: string;
  packagedSkillsRoot?: string;
  sourceOpenClawEntry?: string;
}

const MANAGED_SKILL_PREFIX = "qjclaw-cloud-";

function slugify(value: string): string {
  return value
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 48) || "skill";
}

function stripBom(content: string): string {
  return content.replace(/^\uFEFF/, "");
}

function extractFrontmatterName(content: string): string | undefined {
  const trimmed = stripBom(content);
  if (!trimmed.startsWith("---")) {
    return undefined;
  }

  const endIndex = trimmed.indexOf("\n---", 3);
  if (endIndex < 0) {
    return undefined;
  }

  const header = trimmed.slice(3, endIndex);
  const match = header.match(/^name:\s*(.+)$/m);
  return match?.[1]?.trim().replace(/^[\'\"]|[\'\"]$/g, "") || undefined;
}

function applyRuntimeSkillName(content: string, runtimeSkillName: string): string {
  const normalized = stripBom(content);
  if (normalized.startsWith("---")) {
    const endIndex = normalized.indexOf("\n---", 3);
    if (endIndex >= 0) {
      const header = normalized.slice(3, endIndex);
      const body = normalized.slice(endIndex + 4);
      const nextHeader = /^name:\s*.+$/m.test(header)
        ? header.replace(/^name:\s*.+$/m, `name: ${runtimeSkillName}`)
        : `name: ${runtimeSkillName}\n${header.trimStart()}`;
      return `---\n${nextHeader}\n---${body}`;
    }
  }

  return `---\nname: ${runtimeSkillName}\n---\n\n${normalized}`;
}

function sanitizeRuntimeDirName(value: string): string {
  return value
    .trim()
    .replace(/[<>:"/\\|?*\x00-\x1F]/g, "-")
    .replace(/\.+$/g, "")
    .replace(/^\.+/g, "")
    || "skill";
}

export class RuntimeSkillBridgeService {
  private readonly skillStore: SkillStoreService;
  private readonly projectStore: ProjectStoreService;
  private readonly runtimeManager: RuntimeManager;

  constructor(skillStore: SkillStoreService, projectStore: ProjectStoreService, runtimeManager: RuntimeManager) {
    this.skillStore = skillStore;
    this.projectStore = projectStore;
    this.runtimeManager = runtimeManager;
  }

  async preparePrompt(prompt: string, skillId: string, projectId?: string): Promise<PreparedSkillExecution> {
    const target = (projectId
      ? await this.projectStore.getProjectSkillTarget(projectId, skillId)
      : await this.projectStore.getCurrentProjectSkillTarget(skillId))
      ?? await this.skillStore.getExecutionTarget(skillId);
    if (!target) {
      throw new Error("The selected skill is not ready locally yet.");
    }

    const dependencyTargets = projectId
      ? await this.projectStore.getProjectSkillDependencyTargets(projectId, skillId)
      : [];
    const runtimeStatus = await this.runtimeManager.status();
    const activation = await this.activateRuntimeSkills(target, dependencyTargets, runtimeStatus.runtimeDataDir);

    return {
      prompt: this.buildPrompt(prompt, target.name, activation.runtimeSkillName),
      skillName: target.name,
      runtimeSkillName: activation.runtimeSkillName,
      runtimeSkillDir: activation.runtimeSkillDir,
      localPath: target.localPath
    };
  }

  async clearManagedSkills(): Promise<void> {
    const runtimeStatus = await this.runtimeManager.status();
    const skillsRoot = await this.resolveManagedSkillsRoot(runtimeStatus.runtimeDataDir);
    await this.removeManagedSkillDirs(skillsRoot);
  }

  private async activateRuntimeSkills(
    target: SkillExecutionTarget,
    dependencyTargets: SkillExecutionTarget[],
    runtimeDataDir: string
  ): Promise<{ runtimeSkillName: string; runtimeSkillDir: string }> {
    const skillsRoot = await this.resolveManagedSkillsRoot(runtimeDataDir);
    await mkdir(skillsRoot, { recursive: true });
    await this.removeManagedSkillDirs(skillsRoot);

    const mainActivation = await this.materializeRuntimeSkill(target, skillsRoot, true);
    const seenLocalPaths = new Set<string>([path.resolve(target.localPath)]);

    for (const dependencyTarget of dependencyTargets) {
      const resolvedPath = path.resolve(dependencyTarget.localPath);
      if (seenLocalPaths.has(resolvedPath)) {
        continue;
      }
      seenLocalPaths.add(resolvedPath);
      await this.materializeRuntimeSkill(dependencyTarget, skillsRoot, false);
    }

    return mainActivation;
  }

  private async materializeRuntimeSkill(
    target: SkillExecutionTarget,
    skillsRoot: string,
    selected: boolean
  ): Promise<{ runtimeSkillName: string; runtimeSkillDir: string }> {
    const content = await readFile(target.localPath, "utf8");
    const sourceName = extractFrontmatterName(content)
      ?? (target.fileName ? path.parse(target.fileName).name : undefined)
      ?? target.name
      ?? target.skillId;
    const runtimeSkillName = selected
      ? `${MANAGED_SKILL_PREFIX}${slugify(sourceName)}`
      : sourceName;
    const runtimeSkillDirName = `${MANAGED_SKILL_PREFIX}${slugify(target.skillId || target.name || sourceName)}`;
    const runtimeSkillDir = path.join(skillsRoot, sanitizeRuntimeDirName(runtimeSkillDirName));
    const materializedContent = applyRuntimeSkillName(content, runtimeSkillName);
    const sourceSkillDir = path.dirname(target.localPath);

    await rm(runtimeSkillDir, { recursive: true, force: true }).catch(() => undefined);
    await cp(sourceSkillDir, runtimeSkillDir, { recursive: true, force: true });
    await writeFile(path.join(runtimeSkillDir, "SKILL.md"), materializedContent, "utf8");
    await writeFile(path.join(runtimeSkillDir, ".qjclaw-skill.json"), JSON.stringify({
      source: "cloud",
      selected,
      selectedSkillName: target.name,
      runtimeSkillName,
      localPath: target.localPath,
      materializedAt: new Date().toISOString()
    }, null, 2), "utf8");

    return {
      runtimeSkillName,
      runtimeSkillDir
    };
  }

  private async resolveManagedSkillsRoot(runtimeDataDir: string): Promise<string> {
    try {
      const runtimePaths = this.runtimeManager.resolveBundledPaths();
      const manifestPath = path.join(runtimePaths.runtimeDir, "runtime-manifest.json");
      const manifestRaw = await readFile(manifestPath, "utf8");
      const manifest = JSON.parse(stripBom(manifestRaw)) as BundledRuntimeManifestShape;
      const packagedSkillsRoot = typeof manifest.packagedSkillsRoot === "string" ? manifest.packagedSkillsRoot.trim() : "";
      if (packagedSkillsRoot) {
        return path.resolve(runtimePaths.runtimeDir, packagedSkillsRoot);
      }

      const packagedEntry = typeof manifest.packagedOpenClawEntry === "string" ? manifest.packagedOpenClawEntry.trim() : "";
      if (packagedEntry) {
        return path.join(path.dirname(path.resolve(runtimePaths.runtimeDir, packagedEntry)), "skills");
      }

      const sourceEntry = typeof manifest.sourceOpenClawEntry === "string" ? manifest.sourceOpenClawEntry.trim() : "";
      if (sourceEntry) {
        return path.join(path.dirname(sourceEntry), "skills");
      }
    } catch {
      // Fall back to a runtime-local skills directory when the bundled manifest is unavailable.
    }

    return path.join(runtimeDataDir, "skills");
  }

  private buildPrompt(originalPrompt: string, displayName: string, runtimeSkillName: string): string {
    return [
      `You must use the installed OpenClaw skill \"${runtimeSkillName}\" for this request.`,
      `The user explicitly selected the desktop skill \"${displayName}\".`,
      "Do not answer with a generic chat response before trying to invoke that local skill.",
      "",
      "User request:",
      originalPrompt
    ].join("\n");
  }

  private async removeManagedSkillDirs(skillsRoot: string): Promise<void> {
    let entries: Array<{ name: string; isDirectory: () => boolean }> = [];
    try {
      entries = await readdir(skillsRoot, { withFileTypes: true });
    } catch {
      return;
    }

    await Promise.all(
      entries
        .filter((entry) => entry.isDirectory())
        .map(async (entry) => {
          const metadataPath = path.join(skillsRoot, entry.name, ".qjclaw-skill.json");
          try {
            const metadataRaw = await readFile(metadataPath, "utf8");
            const metadata = JSON.parse(stripBom(metadataRaw)) as { source?: string };
            if (metadata.source !== "cloud") {
              return;
            }
          } catch {
            if (!entry.name.startsWith(MANAGED_SKILL_PREFIX)) {
              return;
            }
          }
          await rm(path.join(skillsRoot, entry.name), { recursive: true, force: true });
        })
    );
  }
}
