#!/usr/bin/env node
// scripts/sync-android-docs.js
// Transforms Android in-app docs for publishing on the meshtastic.org Docusaurus site.
//
// Usage:  node scripts/sync-android-docs.js <android-repo-path> [--convert-webp] [--dry-run]
//
// <android-repo-path>  Path to a clone of meshtastic/Meshtastic-Android (or omit to
//                      auto-detect from this script's location in the repo).
// --convert-webp       Convert PNG/JPG/JPEG/GIF images to WebP via cwebp and rewrite
//                      all image references in Markdown to use .webp. Requires cwebp on PATH.
// --dry-run            Print what would be written without actually writing files.
//
// Output structure (relative to CWD, typically the meshtastic/meshtastic repo root):
//   docs/software/android/user/*.md
//   docs/software/android/developer/*.md
//   docs/software/android/index.md
//   static/img/android/docs/*.webp (or .png/.svg if not converting)

"use strict";

const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const { discoverSlugs } = require("./lib/frontmatter");

// ── Configuration ────────────────────────────────────────────────────────────

const args = process.argv.slice(2);
const CONVERT_WEBP = args.includes("--convert-webp");
const DRY_RUN = args.includes("--dry-run");
const positionalArgs = args.filter(a => !a.startsWith("--"));

const WEBP_CONVERTIBLE = new Set([".png", ".jpg", ".jpeg", ".gif"]);
const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"]);

// Resolve source: explicit path argument, or auto-detect from script location
const ANDROID_REPO_ROOT = positionalArgs.length > 0
    ? path.resolve(positionalArgs[0])
    : path.resolve(__dirname, "..");
const SRC_DOCS_DIR = path.join(ANDROID_REPO_ROOT, "docs", "en");
const SRC_SCREENSHOTS_DIR = path.join(ANDROID_REPO_ROOT, "docs", "assets", "screenshots");

if (!fs.existsSync(SRC_DOCS_DIR)) {
    console.error(`Error: docs directory not found at ${SRC_DOCS_DIR}`);
    console.error("Usage: node sync-android-docs.js <android-repo-path> [--convert-webp] [--dry-run]");
    process.exit(1);
}

// Output directories (relative to CWD, which should be the meshtastic/meshtastic repo)
const DEST_DOCS_DIR = path.join("docs", "software", "android");
const DEST_IMAGES_DIR = path.join("static", "img", "android", "docs");

// Derive sibling page slugs from the filesystem (no manual sync needed)
const KNOWN_USER_SLUGS = discoverSlugs(SRC_DOCS_DIR, "user");
const KNOWN_DEV_SLUGS = discoverSlugs(SRC_DOCS_DIR, "developer");

// ── Helpers ──────────────────────────────────────────────────────────────────

function ensureDir(dir) {
    if (!DRY_RUN) {
        fs.mkdirSync(dir, { recursive: true });
    }
}

function writeFile(filePath, content) {
    if (DRY_RUN) {
        console.log(`[dry-run] Would write: ${filePath} (${Buffer.byteLength(content)} bytes)`);
    } else {
        ensureDir(path.dirname(filePath));
        fs.writeFileSync(filePath, content, "utf-8");
        console.log(`Wrote: ${filePath}`);
    }
}

function copyFile(src, dest) {
    if (DRY_RUN) {
        console.log(`[dry-run] Would copy: ${src} → ${dest}`);
    } else {
        ensureDir(path.dirname(dest));
        fs.copyFileSync(src, dest);
        console.log(`Copied: ${dest}`);
    }
}

/**
 * Rewrite image references in markdown to point to /img/android/docs/<file>.
 * When --convert-webp, convertible extensions become .webp.
 */
function rewriteImagePaths(content) {
    function destBasename(imgPath) {
        const base = path.basename(imgPath);
        const ext = path.extname(base).toLowerCase();
        if (CONVERT_WEBP && WEBP_CONVERTIBLE.has(ext)) {
            return base.slice(0, -ext.length) + ".webp";
        }
        return base;
    }

    return content
        .replace(
            /!\[([^\]]*)\]\((?!https?:\/\/)(?!\/img\/)([^)]+)\)/g,
            (match, alt, imgPath) => {
                const ext = path.extname(path.basename(imgPath)).toLowerCase();
                if (!IMAGE_EXTENSIONS.has(ext)) return match;
                return `![${alt}](/img/android/docs/${destBasename(imgPath)})`;
            },
        )
        .replace(
            /<img\s+([^>]*?)src=["'](?!https?:\/\/)(?!\/img\/)([^"']+)["']([^>]*)>/gi,
            (match, before, imgPath, after) => {
                const ext = path.extname(path.basename(imgPath)).toLowerCase();
                if (!IMAGE_EXTENSIONS.has(ext)) return match;
                return `<img ${before}src="/img/android/docs/${destBasename(imgPath)}"${after}>`;
            },
        );
}

/**
 * Rewrite relative markdown links between sibling pages.
 * e.g., `[text](connections)` → `[text](connections.md)`
 * e.g., `[text](../developer/testing)` → `[text](../developer/testing.md)`
 */
function rewriteSiblingLinks(content, section, isIndex = false) {
    const slugs = section === "user" ? KNOWN_USER_SLUGS : KNOWN_DEV_SLUGS;

    // Match [text](link) where link is NOT an absolute URL, NOT an anchor, NOT already .md
    return content.replace(
        /\[([^\]]*)\]\((?!https?:\/\/)(?!#)([^)]+)\)/g,
        (match, text, link) => {
            // Split off a trailing #anchor (or ?query) so it survives the .md rewrite.
            // Source links like `node-metrics#air-quality-metrics` must become
            // `node-metrics.md#air-quality-metrics`, else Docusaurus resolves the slug
            // as a sub-path of the current page and the build fails on a broken link.
            const splitIdx = link.search(/[#?]/);
            const linkPath = splitIdx >= 0 ? link.slice(0, splitIdx) : link;
            const suffix = splitIdx >= 0 ? link.slice(splitIdx) : "";

            // Skip if already has .md extension or is an image
            if (linkPath.endsWith(".md") || IMAGE_EXTENSIONS.has(path.extname(linkPath).toLowerCase())) {
                return match;
            }

            // Section landing page (user/index.md, developer/index.md): the source
            // user.md/developer.md sit beside the section dir, so they link to children
            // as "user/onboarding". From index.md inside that dir, strip the prefix.
            if (isIndex) {
                const sameSection = linkPath.match(new RegExp(`^${section}/(.+)`));
                if (sameSection && slugs.has(sameSection[1])) {
                    return `[${text}](${sameSection[1]}.md${suffix})`;
                }
            }

            // Check for cross-section links like ../developer/testing
            const crossMatch = linkPath.match(/^\.\.\/(\w+)\/(.+)/);
            if (crossMatch) {
                const [, targetSection, slug] = crossMatch;
                const targetSlugs = targetSection === "user" ? KNOWN_USER_SLUGS : KNOWN_DEV_SLUGS;
                if (targetSlugs.has(slug)) {
                    return `[${text}](../${targetSection}/${slug}.md${suffix})`;
                }
            }

            // Check for sibling links
            const bare = linkPath.replace(/^\.\//, "");
            if (slugs.has(bare)) {
                return `[${text}](${bare}.md${suffix})`;
            }

            return match;
        },
    );
}

/**
 * Transform Jekyll/kramdown frontmatter to Docusaurus-compatible format.
 * Strips `parent`, `aliases`, and remaps `nav_order` to `sidebar_position`.
 */
function transformFrontmatter(content, section) {
    const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
    if (!fmMatch) return content;

    const fmBlock = fmMatch[1];
    const body = content.slice(fmMatch[0].length);

    const lines = fmBlock.split("\n");
    const newLines = [];
    let sidebarPosition = null;

    for (const line of lines) {
        const trimmed = line.trim();

        // Skip Jekyll-specific fields
        if (trimmed.startsWith("parent:")) continue;
        if (trimmed.startsWith("aliases:")) continue;
        if (trimmed.startsWith("- ")) continue; // alias list items

        // Remap nav_order → sidebar_position
        const navMatch = trimmed.match(/^nav_order:\s*(\d+)/);
        if (navMatch) {
            sidebarPosition = navMatch[1];
            newLines.push(`sidebar_position: ${sidebarPosition}`);
            continue;
        }

        if (trimmed) newLines.push(line);
    }

    // Add parent reference for Docusaurus category
    const parentTitle = section === "user" ? "User Guide" : "Developer Guide";
    newLines.push(`parent: ${parentTitle}`);

    return `---\n${newLines.join("\n")}\n---\n${body}`;
}

/**
 * Convert Jekyll-style callouts to Docusaurus admonitions.
 * > **Tip — text** → :::tip\ntext\n:::
 */
function convertCallouts(content) {
    // Match blockquotes starting with **Tip/Note/Warning —
    return content.replace(
        /^(> \*\*(Tip|Note|Warning)\s*[—–-]\s*)([^*]*)\*\*\s*([\s\S]*?)(?=\n(?!>)|$)/gm,
        (match, prefix, type, title, body) => {
            const admonitionType = type.toLowerCase();
            const cleanBody = body.replace(/^>\s?/gm, "").trim();
            const fullContent = title.trim() ? `${title.trim()} ${cleanBody}` : cleanBody;
            return `:::${admonitionType}\n${fullContent}\n:::`;
        },
    );
}

// ── Main ─────────────────────────────────────────────────────────────────────

function processMarkdown(srcPath, destPath, section, isIndex = false) {
    let content = fs.readFileSync(srcPath, "utf-8");
    content = transformFrontmatter(content, section);
    content = rewriteImagePaths(content);
    content = rewriteSiblingLinks(content, section, isIndex);
    content = convertCallouts(content);
    writeFile(destPath, content);
}

/** Sync screenshots, returning the set of destination basenames written. */
function processImages() {
    const written = new Set();
    if (!fs.existsSync(SRC_SCREENSHOTS_DIR)) {
        console.log("No screenshots directory found, skipping image sync.");
        return written;
    }

    const images = fs.readdirSync(SRC_SCREENSHOTS_DIR)
        .filter(f => IMAGE_EXTENSIONS.has(path.extname(f).toLowerCase()));

    for (const img of images) {
        const srcPath = path.join(SRC_SCREENSHOTS_DIR, img);
        const ext = path.extname(img).toLowerCase();

        if (CONVERT_WEBP && WEBP_CONVERTIBLE.has(ext)) {
            const destName = img.slice(0, -ext.length) + ".webp";
            const destPath = path.join(DEST_IMAGES_DIR, destName);

            if (DRY_RUN) {
                console.log(`[dry-run] Would convert: ${srcPath} → ${destPath}`);
                written.add(destName);
            } else {
                ensureDir(path.dirname(destPath));
                try {
                    execSync(`cwebp -q 80 "${srcPath}" -o "${destPath}"`, { stdio: "pipe" });
                    console.log(`Converted: ${destPath}`);
                    written.add(destName);
                } catch (err) {
                    console.error(`Failed to convert ${img}: ${err.message}`);
                    // Fall back to copying the original
                    copyFile(srcPath, path.join(DEST_IMAGES_DIR, img));
                    written.add(img);
                }
            }
        } else {
            copyFile(srcPath, path.join(DEST_IMAGES_DIR, img));
            written.add(img);
        }
    }
    return written;
}

/** Recursively collect files under a directory, as paths relative to it. */
function collectFiles(dir) {
    const results = [];
    if (!fs.existsSync(dir)) return results;
    (function walk(current) {
        for (const entry of fs.readdirSync(current)) {
            const full = path.join(current, entry);
            if (fs.statSync(full).isDirectory()) walk(full);
            else results.push(path.relative(dir, full));
        }
    })(dir);
    return results;
}

/**
 * Remove destination files this run did not produce, so renamed or deleted
 * source pages (and the screenshots they referenced) don't linger on the site.
 * Mirrors the Apple sync's cleanup pass. Scoped to docs pages (.md/.mdx) and
 * images under the directories this script owns.
 */
function pruneStale(expectedDocPaths, expectedImageNames) {
    for (const file of collectFiles(DEST_DOCS_DIR)) {
        const ext = path.extname(file).toLowerCase();
        if (ext !== ".md" && ext !== ".mdx") continue;
        if (expectedDocPaths.has(file.split(path.sep).join("/"))) continue;
        const target = path.join(DEST_DOCS_DIR, file);
        if (DRY_RUN) {
            console.log(`[dry-run] Would remove stale page: ${target}`);
        } else {
            fs.unlinkSync(target);
            console.log(`Removed stale page: ${target}`);
        }
    }
    for (const file of collectFiles(DEST_IMAGES_DIR)) {
        if (!IMAGE_EXTENSIONS.has(path.extname(file).toLowerCase())) continue;
        if (expectedImageNames.has(path.basename(file))) continue;
        const target = path.join(DEST_IMAGES_DIR, file);
        if (DRY_RUN) {
            console.log(`[dry-run] Would remove stale image: ${target}`);
        } else {
            fs.unlinkSync(target);
            console.log(`Removed stale image: ${target}`);
        }
    }
}

function createIndexPage() {
    const content = `---
title: Android App
sidebar_position: 1
---

# Meshtastic Android & Desktop App

Documentation for the [Meshtastic Android](https://github.com/meshtastic/Meshtastic-Android) application, also available as a Desktop (JVM) app for Linux, macOS, and Windows.

## Guides

- **[User Guide](user/)** — Setup, messaging, nodes, maps, settings, and more
- **[Developer Guide](developer/)** — Architecture, KMP conventions, testing, and contributing
`;
    writeFile(path.join(DEST_DOCS_DIR, "index.md"), content);
}

function createCategoryFiles() {
    // Top-level section category. Uses the synced index.md as the landing page
    // (matching the Apple sync), replacing any hand-written generated-index that
    // would otherwise collide with index.md.
    // Unique `key` per category: the "User Guide" / "Developer Guide" labels also
    // exist in the Apple section, and Docusaurus derives sidebar translation keys
    // from the label, so without a key the two sections collide at build time.
    const sectionCategory = `key: androidApp
label: Android App
collapsible: true
position: 1
link:
  type: doc
  id: software/android/index
`;
    const userCategory = `key: androidUserGuide
label: User Guide
collapsible: true
position: 1
link:
  type: doc
  id: software/android/user/index
`;
    const devCategory = `key: androidDeveloperGuide
label: Developer Guide
collapsible: true
position: 2
link:
  type: doc
  id: software/android/developer/index
`;
    writeFile(path.join(DEST_DOCS_DIR, "_category_.yml"), sectionCategory);
    writeFile(path.join(DEST_DOCS_DIR, "user", "_category_.yml"), userCategory);
    writeFile(path.join(DEST_DOCS_DIR, "developer", "_category_.yml"), devCategory);
}

function main() {
    console.log(`Source: ${SRC_DOCS_DIR}`);
    console.log(`Destination: ${DEST_DOCS_DIR}`);
    console.log(`WebP conversion: ${CONVERT_WEBP ? "enabled" : "disabled"}`);
    console.log(`Dry run: ${DRY_RUN}`);
    console.log("");

    // Track everything this run produces so stale files can be pruned afterwards.
    const expectedDocPaths = new Set([
        "index.md",
        "_category_.yml",
        "user/_category_.yml",
        "developer/_category_.yml",
    ]);

    // Section overview pages: docs/en/user.md → user/index.md and
    // docs/en/developer.md → developer/index.md. Docusaurus uses <dir>/index.md
    // as the category landing page, so the index.md "User Guide" / "Developer
    // Guide" links resolve (matching the Apple sync).
    const sections = [
        { src: "user.md", dest: ["user", "index.md"], section: "user", key: "user/index.md" },
        { src: "developer.md", dest: ["developer", "index.md"], section: "developer", key: "developer/index.md" },
    ];
    for (const { src, dest, section, key } of sections) {
        const overview = path.join(SRC_DOCS_DIR, src);
        if (fs.existsSync(overview)) {
            processMarkdown(overview, path.join(DEST_DOCS_DIR, ...dest), section, true);
            expectedDocPaths.add(key);
        }
    }

    // Process user guide
    const userDir = path.join(SRC_DOCS_DIR, "user");
    if (fs.existsSync(userDir)) {
        for (const file of fs.readdirSync(userDir).filter(f => f.endsWith(".md"))) {
            processMarkdown(
                path.join(userDir, file),
                path.join(DEST_DOCS_DIR, "user", file),
                "user",
            );
            expectedDocPaths.add(`user/${file}`);
        }
    }

    // Process developer guide
    const devDir = path.join(SRC_DOCS_DIR, "developer");
    if (fs.existsSync(devDir)) {
        for (const file of fs.readdirSync(devDir).filter(f => f.endsWith(".md"))) {
            processMarkdown(
                path.join(devDir, file),
                path.join(DEST_DOCS_DIR, "developer", file),
                "developer",
            );
            expectedDocPaths.add(`developer/${file}`);
        }
    }

    // Create index and category files
    createIndexPage();
    createCategoryFiles();

    // Process images
    const expectedImageNames = processImages();

    // Remove anything left over from a previous sync or the old hand-written docs.
    pruneStale(expectedDocPaths, expectedImageNames);

    console.log("\nSync complete.");
}

main();
