import { randomBytes } from "node:crypto";
import {
  existsSync,
  lstatSync,
  mkdirSync,
  readdirSync,
  readFileSync,
  readlinkSync,
  rmSync,
  symlinkSync
} from "node:fs";
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";

import {
  boundSkillText,
  firstManifestField,
  firstMarkdownSummary,
  parseFrontmatter
} from "./codex-stdio-session-helpers.ts";
import { createHwlabAgentRunAssemblySummary } from "../agent/agentrun-dispatch.mjs";

export const DEFAULT_PREINSTALLED_SKILLS_DIR = "/app/skills";
export const DEFAULT_USER_SKILLS_DIR = "/data/user-skills";
export const USER_SKILL_METADATA_FILE = ".hwlab-skill.json";
export const USER_SKILLS_INDEX_FILE = ".hwlab-user-skills.json";

const DEFAULT_CODE_AGENT_WORKSPACE = "/workspace/hwlab";

export function configureSkillRuntime(env = process.env) {
  const config = skillRuntimeConfig(env);
  env.HWLAB_PREINSTALLED_SKILLS_DIR ||= config.preinstalledDir;
  env.HWLAB_USER_SKILLS_DIR ||= config.userSkillsDir;

  const configured = splitSkillDirs(env.HWLAB_CODE_AGENT_SKILLS_DIRS || env.UNIDESK_SKILLS_PATH || "");
  const strict = env.HWLAB_CODE_AGENT_SKILLS_STRICT === "1";
  const nextDirs = strict && configured.length > 0
    ? configured
    : [...configured, config.preinstalledDir, config.userSkillsDir];
  env.HWLAB_CODE_AGENT_SKILLS_DIRS = uniquePaths(nextDirs).join(path.delimiter);
  return config;
}

export function skillRuntimeConfig(env = process.env) {
  const workspace = path.resolve(String(env.HWLAB_CODE_AGENT_CODEX_WORKSPACE || env.HWLAB_CODE_AGENT_WORKSPACE || DEFAULT_CODE_AGENT_WORKSPACE));
  const preinstalledDir = path.resolve(String(env.HWLAB_PREINSTALLED_SKILLS_DIR || DEFAULT_PREINSTALLED_SKILLS_DIR));
  const userSkillsDir = path.resolve(String(env.HWLAB_USER_SKILLS_DIR || DEFAULT_USER_SKILLS_DIR));
  return {
    workspace,
    preinstalledDir,
    userSkillsDir,
    codexSkillsDir: path.join(workspace, ".agents", "skills")
  };
}

export function splitSkillDirs(value) {
  return String(value ?? "")
    .split(/[,;]/u)
    .flatMap((part) => part.split(path.delimiter))
    .map((dir) => dir.trim())
    .filter(Boolean);
}

export async function listSkills(options = {}) {
  const env = options.env ?? process.env;
  const config = skillRuntimeConfig(env);
  await mkdir(config.userSkillsDir, { recursive: true });
  const [preinstalled, uploaded] = await Promise.all([
    listPreinstalledSkills(config.preinstalledDir),
    listUploadedSkills(config.userSkillsDir)
  ]);
  ensureCodexSkillsAggregationSync(config);
  const skills = [...preinstalled, ...uploaded].sort(compareSkillItems);
  return {
    serviceId: "hwlab-cloud-api",
    route: "/v1/skills",
    status: "ok",
    roots: {
      preinstalled: config.preinstalledDir,
      uploaded: config.userSkillsDir,
      codexAggregation: config.codexSkillsDir
    },
    agentRunAssembly: createHwlabAgentRunAssemblySummary({ env }),
    count: skills.length,
    skills
  };
}

export async function uploadUserSkill(payload, options = {}) {
  const env = options.env ?? process.env;
  const config = skillRuntimeConfig(env);
  const prepared = prepareUploadedFiles(payload?.files);
  if (prepared.length === 0) {
    throw httpError(400, "invalid_skill_upload", "Skill upload body must include files.");
  }

  await mkdir(config.userSkillsDir, { recursive: true });
  const id = await allocateUserSkillId(config.userSkillsDir);
  const rootDir = path.join(config.userSkillsDir, id);
  await mkdir(rootDir, { recursive: false });

  let sizeBytes = 0;
  try {
    for (const file of prepared) {
      const target = safeJoin(rootDir, file.relativePath);
      if (!target) {
        throw httpError(400, "invalid_skill_path", `Skill upload file path is outside the skill root: ${file.relativePath}`);
      }
      await mkdir(path.dirname(target), { recursive: true });
      await writeFile(target, file.content, { flag: "wx" });
      sizeBytes += file.content.length;
    }

    const now = new Date().toISOString();
    const manifest = await readSkillManifestSummary(path.join(rootDir, "SKILL.md"));
    const metadata = {
      id,
      source: "uploaded",
      name: nonEmptyString(payload?.name) ?? manifest?.name ?? id,
      description: manifest?.description ?? "Uploaded skill",
      version: manifest?.version ?? null,
      commitId: manifest?.commitId ?? null,
      rootDir,
      manifestPath: existsSync(path.join(rootDir, "SKILL.md")) ? path.join(rootDir, "SKILL.md") : null,
      createdAt: now,
      updatedAt: now,
      fileCount: prepared.length,
      sizeBytes
    };
    await writeJson(path.join(rootDir, USER_SKILL_METADATA_FILE), metadata);
    await writeUserSkillsIndex(config.userSkillsDir);
    ensureCodexSkillsAggregationSync(config);
    return {
      serviceId: "hwlab-cloud-api",
      route: "/v1/skills/uploads",
      status: "ok",
      accepted: true,
      skill: metadata
    };
  } catch (error) {
    await rmIfEmptyOrCreated(rootDir);
    throw error;
  }
}

export async function getSkillTree(id, options = {}) {
  const skill = await resolveSkillById(id, options);
  if (!skill) throw httpError(404, "skill_not_found", `Skill not found: ${id}`);
  const tree = await walkSkillTree(skill.rootDir);
  return {
    serviceId: "hwlab-cloud-api",
    route: `/v1/skills/${encodeURIComponent(id)}/tree`,
    status: "ok",
    skill,
    tree,
    count: tree.length
  };
}

export async function readSkillTextFile(id, relativePath = "SKILL.md", options = {}) {
  const skill = await resolveSkillById(id, options);
  if (!skill) throw httpError(404, "skill_not_found", `Skill not found: ${id}`);
  const safeRelativePath = nonEmptyString(relativePath) ?? "SKILL.md";
  const filePath = safeJoin(skill.rootDir, safeRelativePath);
  if (!filePath) throw httpError(400, "invalid_skill_path", "Requested skill file path is outside the skill root.");
  let content;
  try {
    content = await readFile(filePath);
  } catch {
    throw httpError(404, "skill_file_not_found", `Skill file not found: ${safeRelativePath}`);
  }
  if (looksBinary(content)) {
    throw httpError(415, "skill_file_binary", "Binary skill files are not returned inline.");
  }
  return {
    serviceId: "hwlab-cloud-api",
    route: `/v1/skills/${encodeURIComponent(id)}/files`,
    status: "ok",
    skill,
    file: {
      path: normalizeRelativePath(safeRelativePath),
      sizeBytes: content.length,
      contentType: "text/plain; charset=utf-8",
      content: content.toString("utf8")
    }
  };
}

export async function resolveSkillById(id, options = {}) {
  const skillId = nonEmptyString(id);
  if (!skillId) return null;
  const payload = await listSkills(options);
  return payload.skills.find((skill) => skill.id === skillId) ?? null;
}

export async function listPreinstalledSkills(rootDir) {
  return listSkillRoots(rootDir, "preinstalled");
}

export async function listUploadedSkills(rootDir) {
  await mkdir(rootDir, { recursive: true });
  return listSkillRoots(rootDir, "uploaded");
}

export function ensureCodexSkillsAggregationSync(input = process.env) {
  const config = input?.codexSkillsDir ? input : skillRuntimeConfig(input);
  try {
    mkdirSync(config.codexSkillsDir, { recursive: true });
  } catch {
    return;
  }

  for (const record of skillRootRecordsSync(config.preinstalledDir, "preinstalled")) {
    const linkName = `preinstalled-${safeLinkSegment(record.name)}`;
    upsertSkillSymlink(path.join(config.codexSkillsDir, linkName), record.rootDir);
  }
  for (const record of skillRootRecordsSync(config.userSkillsDir, "uploaded")) {
    const linkName = `uploaded-${safeLinkSegment(record.id)}`;
    upsertSkillSymlink(path.join(config.codexSkillsDir, linkName), record.rootDir);
  }
}

async function listSkillRoots(rootDir, source) {
  const records = [];
  const root = path.resolve(rootDir);
  if (!existsSync(root)) return records;
  if (existsSync(path.join(root, "SKILL.md"))) {
    records.push(await skillItemFromRoot({ rootDir: root, source, idSeed: path.basename(root) || source }));
  }

  let entries = [];
  try {
    entries = await readdir(root, { withFileTypes: true });
  } catch {
    return records;
  }

  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    const childRoot = path.join(root, entry.name);
    if (source === "preinstalled" && !existsSync(path.join(childRoot, "SKILL.md"))) continue;
    if (source === "uploaded" && !isUploadedSkillRoot(childRoot, entry.name)) continue;
    records.push(await skillItemFromRoot({ rootDir: childRoot, source, idSeed: entry.name }));
  }
  return records.filter(Boolean);
}

async function skillItemFromRoot({ rootDir, source, idSeed }) {
  const manifestPath = path.join(rootDir, "SKILL.md");
  const manifest = await readSkillManifestSummary(manifestPath);
  const metadata = source === "uploaded" ? await readJson(path.join(rootDir, USER_SKILL_METADATA_FILE)) : null;
  const id = source === "uploaded"
    ? nonEmptyString(metadata?.id) ?? path.basename(rootDir)
    : `preinstalled-${safeLinkSegment(idSeed)}`;
  const stats = await skillRootStats(rootDir);
  return {
    id,
    source,
    name: nonEmptyString(metadata?.name) ?? manifest?.name ?? path.basename(rootDir),
    description: nonEmptyString(metadata?.description) ?? manifest?.description ?? "No description provided.",
    version: nonEmptyString(metadata?.version) ?? manifest?.version ?? null,
    commitId: nonEmptyString(metadata?.commitId) ?? manifest?.commitId ?? null,
    rootDir,
    manifestPath: existsSync(manifestPath) ? manifestPath : null,
    createdAt: nonEmptyString(metadata?.createdAt) ?? null,
    updatedAt: nonEmptyString(metadata?.updatedAt) ?? null,
    fileCount: Number.isInteger(metadata?.fileCount) ? metadata.fileCount : stats.fileCount,
    sizeBytes: Number.isFinite(Number(metadata?.sizeBytes)) ? Number(metadata.sizeBytes) : stats.sizeBytes
  };
}

async function readSkillManifestSummary(manifestPath) {
  try {
    const text = await readFile(manifestPath, "utf8");
    const frontmatter = parseFrontmatter(text);
    const body = text.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/u, "");
    const name = nonEmptyString(frontmatter.name) ?? path.basename(path.dirname(manifestPath));
    return {
      name,
      description: boundSkillText(frontmatter.description ?? firstMarkdownSummary(body) ?? "No description provided."),
      version: firstManifestField(frontmatter, ["version", "skillVersion"]),
      commitId: firstManifestField(frontmatter, ["commitId", "commit", "gitCommit", "revision"])
    };
  } catch {
    return null;
  }
}

function prepareUploadedFiles(files) {
  const rawFiles = Array.isArray(files) ? files : [];
  const prepared = [];
  for (const file of rawFiles) {
    const relativePath = normalizeRelativePath(file?.relativePath ?? file?.path ?? file?.name);
    if (!relativePath) continue;
    const content = decodeUploadContent(file);
    if (!content) continue;
    prepared.push({ relativePath, content });
  }
  return stripSingleTopLevelSkillFolder(prepared);
}

function stripSingleTopLevelSkillFolder(files) {
  if (files.some((file) => file.relativePath === "SKILL.md")) return files;
  const firstParts = files.map((file) => file.relativePath.split("/")).filter((parts) => parts.length > 1);
  if (firstParts.length !== files.length) return files;
  const [top] = firstParts[0] ?? [];
  if (!top || firstParts.some((parts) => parts[0] !== top)) return files;
  if (!files.some((file) => file.relativePath === `${top}/SKILL.md`)) return files;
  return files.map((file) => ({ ...file, relativePath: file.relativePath.slice(top.length + 1) }));
}

function decodeUploadContent(file) {
  if (typeof file?.contentBase64 === "string") return Buffer.from(file.contentBase64, "base64");
  if (typeof file?.contentText === "string") return Buffer.from(file.contentText, "utf8");
  if (typeof file?.content === "string") return Buffer.from(file.content, "utf8");
  return null;
}

async function allocateUserSkillId(rootDir) {
  for (let attempt = 0; attempt < 8; attempt += 1) {
    const id = `skill-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
    if (!existsSync(path.join(rootDir, id))) return id;
  }
  throw httpError(500, "skill_id_allocation_failed", "Could not allocate uploaded skill id.");
}

async function writeUserSkillsIndex(rootDir) {
  const skills = await listSkillRoots(rootDir, "uploaded");
  await writeJson(path.join(rootDir, USER_SKILLS_INDEX_FILE), {
    version: 1,
    updatedAt: new Date().toISOString(),
    skills: skills.map((skill) => ({
      id: skill.id,
      name: skill.name,
      source: skill.source,
      rootDir: skill.rootDir,
      manifestPath: skill.manifestPath,
      createdAt: skill.createdAt,
      updatedAt: skill.updatedAt,
      fileCount: skill.fileCount,
      sizeBytes: skill.sizeBytes
    }))
  });
}

async function walkSkillTree(rootDir, options = {}) {
  const limit = Number.isInteger(options.limit) ? options.limit : 500;
  const result = [];
  async function walk(current, relativeRoot, depth) {
    if (result.length >= limit || depth > 8) return;
    let entries = [];
    try {
      entries = await readdir(current, { withFileTypes: true });
    } catch {
      return;
    }
    for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name, "en"))) {
      if (entry.name === USER_SKILL_METADATA_FILE || entry.name === USER_SKILLS_INDEX_FILE) continue;
      const relativePath = relativeRoot ? `${relativeRoot}/${entry.name}` : entry.name;
      const fullPath = path.join(current, entry.name);
      if (entry.isDirectory()) {
        result.push({ path: relativePath, type: "directory" });
        await walk(fullPath, relativePath, depth + 1);
      } else if (entry.isFile()) {
        const buffer = await readFile(fullPath).catch(() => null);
        result.push({ path: relativePath, type: "file", sizeBytes: buffer?.length ?? null });
      }
      if (result.length >= limit) return;
    }
  }
  await walk(rootDir, "", 0);
  return result;
}

async function skillRootStats(rootDir) {
  const tree = await walkSkillTree(rootDir, { limit: 500 });
  return {
    fileCount: tree.filter((entry) => entry.type === "file").length,
    sizeBytes: tree.reduce((total, entry) => total + (Number(entry.sizeBytes) || 0), 0)
  };
}

function skillRootRecordsSync(rootDir, source) {
  const root = path.resolve(rootDir);
  if (!existsSync(root)) return [];
  const records = [];
  if (existsSync(path.join(root, "SKILL.md"))) {
    records.push(skillRootRecordSync(root, source, path.basename(root) || source));
  }
  let entries = [];
  try {
    entries = readdirSync(root, { withFileTypes: true });
  } catch {
    return records.filter(Boolean);
  }
  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    const childRoot = path.join(root, entry.name);
    if (source === "preinstalled" && !existsSync(path.join(childRoot, "SKILL.md"))) continue;
    if (source === "uploaded" && !isUploadedSkillRoot(childRoot, entry.name)) continue;
    records.push(skillRootRecordSync(childRoot, source, entry.name));
  }
  return records.filter(Boolean);
}

function skillRootRecordSync(rootDir, source, idSeed) {
  const metadata = source === "uploaded" ? readJsonSync(path.join(rootDir, USER_SKILL_METADATA_FILE)) : null;
  const manifest = readSkillManifestSummarySync(path.join(rootDir, "SKILL.md"));
  return {
    id: source === "uploaded" ? nonEmptyString(metadata?.id) ?? path.basename(rootDir) : `preinstalled-${safeLinkSegment(idSeed)}`,
    name: nonEmptyString(metadata?.name) ?? manifest?.name ?? path.basename(rootDir),
    rootDir
  };
}

function readSkillManifestSummarySync(manifestPath) {
  try {
    const text = readFileSync(manifestPath, "utf8");
    const frontmatter = parseFrontmatter(text);
    return {
      name: nonEmptyString(frontmatter.name) ?? path.basename(path.dirname(manifestPath))
    };
  } catch {
    return null;
  }
}

function isUploadedSkillRoot(rootDir, name) {
  if (name === "lost+found") return false;
  return name.startsWith("skill-") ||
    existsSync(path.join(rootDir, USER_SKILL_METADATA_FILE)) ||
    existsSync(path.join(rootDir, "SKILL.md"));
}

function upsertSkillSymlink(linkPath, target) {
  try {
    const current = lstatSync(linkPath);
    if (!current.isSymbolicLink()) return;
    const linked = path.resolve(path.dirname(linkPath), readlinkSync(linkPath));
    if (linked === path.resolve(target)) return;
    rmSync(linkPath, { force: true });
  } catch {
    // Missing links are created below.
  }
  try {
    symlinkSync(target, linkPath, "dir");
  } catch {
    // Runtime health and API list expose source roots; symlink aggregation is best-effort.
  }
}

function normalizeRelativePath(value) {
  const text = String(value ?? "").replace(/\\/gu, "/").replace(/^\/+/, "").trim();
  const parts = text.split("/").filter(Boolean);
  if (parts.length === 0 || parts.some((part) => part === "." || part === "..")) return null;
  return parts.join("/");
}

function safeJoin(rootDir, relativePath) {
  const normalized = normalizeRelativePath(relativePath);
  if (!normalized) return null;
  const root = path.resolve(rootDir);
  const target = path.resolve(root, normalized);
  return target === root || target.startsWith(`${root}${path.sep}`) ? target : null;
}

function looksBinary(buffer) {
  if (!Buffer.isBuffer(buffer)) return false;
  return buffer.subarray(0, Math.min(buffer.length, 8000)).includes(0);
}

async function readJson(filePath) {
  try {
    return JSON.parse(await readFile(filePath, "utf8"));
  } catch {
    return null;
  }
}

function readJsonSync(filePath) {
  try {
    return JSON.parse(readFileSync(filePath, "utf8"));
  } catch {
    return null;
  }
}

async function writeJson(filePath, value) {
  await mkdir(path.dirname(filePath), { recursive: true });
  await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}

async function rmIfEmptyOrCreated(rootDir) {
  await rm(rootDir, { recursive: true, force: true }).catch(() => {});
}

function nonEmptyString(value) {
  const text = typeof value === "string" ? value.trim() : "";
  return text || null;
}

function uniquePaths(values) {
  const seen = new Set();
  const result = [];
  for (const value of values) {
    const text = String(value ?? "").trim();
    if (!text) continue;
    const resolved = path.resolve(text);
    if (seen.has(resolved)) continue;
    seen.add(resolved);
    result.push(resolved);
  }
  return result;
}

function compareSkillItems(a, b) {
  return String(a.name).localeCompare(String(b.name), "en") ||
    String(a.source).localeCompare(String(b.source), "en") ||
    String(a.id).localeCompare(String(b.id), "en");
}

function safeLinkSegment(value) {
  return String(value ?? "skill")
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9._-]+/giu, "-")
    .replace(/^-+|-+$/gu, "")
    .slice(0, 80) || "skill";
}

function httpError(statusCode, code, message) {
  const error = new Error(message);
  Object.assign(error, { statusCode, code });
  return error;
}
