#!/usr/bin/env node
import { existsSync, mkdirSync, readFileSync, writeFileSync, watch } from "node:fs";
import { homedir } from "node:os";
import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";

const DEFAULT_API_BASE = "http://127.0.0.1:3001/api";
const DEFAULT_APP_BASE = "http://127.0.0.1:5173";
const SIDECAR_NAME = ".htmlslide-sync.json";
const CONFIG_DIR = join(homedir(), ".htmlslide-agent");
const CONFIG_PATH = join(CONFIG_DIR, "config.json");

const parsed = parseArgs(process.argv.slice(2));
const command = parsed.command || "push";

if (command === "login") {
  await login(parsed).catch(fail);
} else if (command === "list") {
  await listProjects(parsed).catch(fail);
} else if (command === "pull") {
  await pullProject(parsed).catch(fail);
} else if (command === "push") {
  await pushProject(parsed).catch(fail);
} else {
  usage(`Unknown command: ${command}`);
}

async function login(args) {
  const config = readConfig();
  const apiBase = stripTrailingSlash(args.apiBase || process.env.HTMLSLIDE_API_BASE || config.apiBase || DEFAULT_API_BASE);
  const appBase = stripTrailingSlash(args.appBase || process.env.HTMLSLIDE_APP_BASE || config.appBase || DEFAULT_APP_BASE);
  const deviceName = args.deviceName || `${process.env.COMPUTERNAME || process.env.HOSTNAME || "Local"} Agent`;

  const start = await apiJson(`${apiBase}/agent/oauth/start`, {
    method: "POST",
    body: { deviceName }
  });

  const url = `${appBase}${start.verificationUriComplete || `/agent/authorize?code=${encodeURIComponent(start.userCode)}`}`;
  console.log("Open this URL in HTMLSlide while logged in:");
  console.log(url);
  console.log(`Authorization code: ${start.userCode}`);
  console.log("Waiting for approval...");

  const deadline = Date.now() + start.expiresIn * 1000;
  while (Date.now() < deadline) {
    await sleep((start.interval || 2) * 1000);
    const tokenRes = await apiJson(`${apiBase}/agent/oauth/token`, {
      method: "POST",
      body: { deviceCode: start.deviceCode },
      soft: true
    });
    if (tokenRes.ok === false) continue;
    const nextConfig = {
      ...config,
      apiBase,
      appBase,
      accessToken: tokenRes.accessToken,
      userId: tokenRes.userId,
      tokenType: tokenRes.tokenType || "Bearer",
      updatedAt: new Date().toISOString()
    };
    writeConfig(nextConfig);
    console.log(`HTMLSlide agent login complete. Bound user: ${tokenRes.userId}`);
    return;
  }

  throw new Error("Authorization timed out. Run login again.");
}

async function listProjects(args) {
  const client = authedClient(args);
  const projects = await client.get("/projects");
  projects.forEach((project) => {
    console.log(`${project.id}\t${project.kind || "slides"}\t${project.title}\t${project.updatedAt || ""}`);
  });
}

async function pullProject(args) {
  const outPath = args.out ? resolve(args.out) : resolve("ppt", "index.html");
  const client = authedClient(args);
  const project = await resolveProject(client, args);
  if (project.kind !== "html" || !project.html) {
    throw new Error(`Project "${project.title}" is not an HTML PPT project.`);
  }

  const outDir = dirname(outPath);
  if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
  writeFileSync(outPath, project.html, "utf8");
  writeSidecar(join(outDir, SIDECAR_NAME), {
    apiBase: client.apiBase,
    projectId: project.id,
    title: project.title,
    originalFileName: project.originalFileName || "index.html",
    pulledAt: new Date().toISOString()
  });

  console.log(`[htmlslide-sync] pulled "${project.title}" -> ${outPath}`);
}

async function pushProject(args) {
  const htmlFile = args.file ? resolve(args.file) : null;
  if (!htmlFile) usage("Missing HTML file. Example: node sync-htmlslide.mjs push path/to/ppt/index.html");
  if (!existsSync(htmlFile)) throw new Error(`HTML file not found: ${htmlFile}`);

  const deckDir = dirname(htmlFile);
  const sidecarPath = join(deckDir, SIDECAR_NAME);
  let sidecar = readJson(sidecarPath, {});
  const client = authedClient({ ...sidecar, ...args });
  const watchMode = Boolean(args.watch);

  let syncing = false;
  let pending = false;

  async function syncOnce() {
    if (syncing) {
      pending = true;
      return;
    }
    syncing = true;
    try {
      const { html, assets, title } = buildHtmlPayload(htmlFile, args.title || sidecar.title);
      const result = await client.post("/agent/projects/html", {
        projectId: args.projectId || sidecar.projectId,
        title,
        html,
        assets,
        originalFileName: basename(htmlFile),
        source: "guizang-ppt-skill 同步",
        author: "本地 Agent"
      });

      sidecar = {
        ...sidecar,
        apiBase: client.apiBase,
        projectId: result.project.id,
        title: result.project.title,
        originalFileName: basename(htmlFile),
        updatedAt: result.project.updatedAt
      };
      writeSidecar(sidecarPath, sidecar);
      console.log(`[htmlslide-sync] pushed "${result.project.title}" -> ${result.project.id}`);
    } finally {
      syncing = false;
      if (pending) {
        pending = false;
        await syncOnce();
      }
    }
  }

  await syncOnce();

  if (watchMode) {
    console.log(`[htmlslide-sync] watching ${deckDir}`);
    let timer = null;
    watch(deckDir, { recursive: true }, (_event, changed) => {
      if (!changed || String(changed).endsWith(SIDECAR_NAME)) return;
      if (timer) clearTimeout(timer);
      timer = setTimeout(() => {
        syncOnce().catch((err) => console.error(`[htmlslide-sync] ${err.message}`));
      }, 450);
    });
  }
}

async function resolveProject(client, args) {
  if (args.projectId) return client.get(`/projects/${encodeURIComponent(args.projectId)}`);
  if (!args.title) throw new Error("Pass --project-id <id> or --title <title>.");
  const projects = await client.get("/projects");
  const exact = projects.filter((project) => project.title === args.title);
  if (exact.length === 1) return exact[0];
  if (exact.length > 1) throw new Error(`Multiple projects named "${args.title}". Use --project-id.`);
  const fuzzy = projects.filter((project) => project.title.includes(args.title));
  if (fuzzy.length === 1) return fuzzy[0];
  if (fuzzy.length > 1) {
    throw new Error(`Multiple projects match "${args.title}": ${fuzzy.map((project) => project.title).join(", ")}. Use --project-id.`);
  }
  throw new Error(`Project not found: ${args.title}`);
}

function authedClient(args) {
  const config = readConfig();
  const apiBase = stripTrailingSlash(args.apiBase || process.env.HTMLSLIDE_API_BASE || config.apiBase || DEFAULT_API_BASE);
  const accessToken = args.token || process.env.HTMLSLIDE_AGENT_TOKEN || config.accessToken;
  if (!accessToken) {
    throw new Error("Not logged in. Run: node scripts/sync-htmlslide.mjs login");
  }
  return {
    apiBase,
    get: (path) => apiJson(`${apiBase}${path}`, { token: accessToken }),
    post: (path, body) => apiJson(`${apiBase}${path}`, { method: "POST", token: accessToken, body })
  };
}

function buildHtmlPayload(htmlPath, titleOverride) {
  const rawHtml = readFileSync(htmlPath, "utf8");
  const assets = {};
  const html = inlineLocalAssets(rawHtml, htmlPath, assets);
  return {
    html,
    assets,
    title: titleOverride || titleFromHtml(rawHtml) || basename(htmlPath).replace(/\.html?$/i, "")
  };
}

function inlineLocalAssets(html, htmlPath, assets) {
  const root = dirname(htmlPath);
  let next = html;

  next = next.replace(/\b(src|href)=["']([^"']+)["']/gi, (match, attr, ref) => {
    const dataUrl = localRefToDataUrl(ref, root, assets);
    return dataUrl ? `${attr}="${dataUrl}"` : match;
  });

  next = next.replace(/url\((["']?)([^"')]+)\1\)/gi, (match, _quote, ref) => {
    const dataUrl = localRefToDataUrl(ref, root, assets);
    return dataUrl ? `url("${dataUrl}")` : match;
  });

  return next;
}

function localRefToDataUrl(ref, root, assets) {
  if (!ref || ref.startsWith("#") || /^[a-z][a-z0-9+.-]*:/i.test(ref) || ref.startsWith("//")) return "";
  const cleanRef = decodeURIComponent(ref.split("#")[0].split("?")[0]);
  const filePath = resolve(root, cleanRef);
  if (!isInside(root, filePath) || !existsSync(filePath)) return "";
  const ext = extname(filePath).toLowerCase();
  if (!isEmbeddable(ext)) return "";

  const rel = normalize(relative(root, filePath)).split(sep).join("/");
  const mime = mimeFromExt(ext);
  const base64 = readFileSync(filePath).toString("base64");
  const dataUrl = `data:${mime};base64,${base64}`;
  assets[rel] = dataUrl;
  return dataUrl;
}

async function apiJson(url, options = {}) {
  const res = await fetch(url, {
    method: options.method || "GET",
    headers: {
      "content-type": "application/json",
      ...(options.token ? { authorization: `Bearer ${options.token}` } : {})
    },
    body: options.body ? JSON.stringify(options.body) : undefined
  });
  const text = await res.text();
  const body = text ? JSON.parse(text) : {};
  if (!res.ok) {
    if (options.soft) return { ok: false, status: res.status, ...body };
    throw new Error(body.error || `HTMLSlide API ${res.status}`);
  }
  return body;
}

function readConfig() {
  return readJson(CONFIG_PATH, {});
}

function writeConfig(config) {
  if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
  writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf8");
}

function readJson(path, fallback) {
  if (!existsSync(path)) return fallback;
  try {
    return JSON.parse(readFileSync(path, "utf8"));
  } catch {
    return fallback;
  }
}

function writeSidecar(path, data) {
  writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf8");
}

function isInside(root, filePath) {
  const rel = relative(root, filePath);
  return rel && !rel.startsWith("..") && !isAbsolute(rel);
}

function isEmbeddable(ext) {
  return [".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".avif", ".ico", ".woff", ".woff2", ".ttf", ".otf"].includes(ext);
}

function mimeFromExt(ext) {
  return {
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".webp": "image/webp",
    ".gif": "image/gif",
    ".svg": "image/svg+xml",
    ".avif": "image/avif",
    ".ico": "image/x-icon",
    ".woff": "font/woff",
    ".woff2": "font/woff2",
    ".ttf": "font/ttf",
    ".otf": "font/otf"
  }[ext] || "application/octet-stream";
}

function titleFromHtml(html) {
  const match = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
  return match ? decodeHtml(match[1].replace(/\s+/g, " ").trim()) : "";
}

function decodeHtml(value) {
  return value
    .replace(/&amp;/g, "&")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'");
}

function stripTrailingSlash(value) {
  return String(value || "").replace(/\/+$/, "");
}

function sleep(ms) {
  return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
}

function parseArgs(argv) {
  const result = {};
  const commands = new Set(["login", "list", "pull", "push"]);
  if (commands.has(argv[0])) result.command = argv.shift();

  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (arg === "--watch") result.watch = true;
    else if (arg === "--api-base") result.apiBase = argv[++i];
    else if (arg === "--app-base") result.appBase = argv[++i];
    else if (arg === "--device-name") result.deviceName = argv[++i];
    else if (arg === "--token") result.token = argv[++i];
    else if (arg === "--title") result.title = argv[++i];
    else if (arg === "--project-id") result.projectId = argv[++i];
    else if (arg === "--out") result.out = argv[++i];
    else if (!result.file && result.command !== "list" && result.command !== "login") result.file = arg;
    else usage(`Unknown argument: ${arg}`);
  }

  if (!result.command && result.file) result.command = "push";
  return result;
}

function usage(message) {
  if (message) console.error(message);
  console.error(`Usage:
  node scripts/sync-htmlslide.mjs login [--api-base http://127.0.0.1:3001/api] [--app-base http://127.0.0.1:5173]
  node scripts/sync-htmlslide.mjs list
  node scripts/sync-htmlslide.mjs pull --title "Project title" --out path/to/ppt/index.html
  node scripts/sync-htmlslide.mjs pull --project-id <id> --out path/to/ppt/index.html
  node scripts/sync-htmlslide.mjs push path/to/ppt/index.html [--title "Project title"] [--project-id <id>] [--watch]`);
  process.exit(2);
}

function fail(err) {
  console.error(`[htmlslide-sync] ${err.message}`);
  process.exit(1);
}
