#!/usr/bin/env python3
"""
migrate.py — Convert the mirrored ZyrexTrack site into a clean, web-only build
             ready to drop on a new domain.

What it does, in one pass over the mirrored ./site folder:

  1. Injects the Mobile bridge shim into every HTML file (so `Mobile.*` calls
     from the old APK front-end keep working in a plain browser).
  2. Rewrites every hardcoded reference to the old GitHub Pages URL
     (https://YukiDiablo.github.io/yuki_diablo_public) to the new domain,
     and normalises absolute links to root-relative ones.
  3. Adds a <link rel="canonical"> to every page pointing at the new domain
     (skips pages that already have one).
  4. Generates SEO/deploy assets for the NEW domain:
        - robots.txt
        - sitemap.xml
        - 404.html (reuses the app's own error page)
        - .htaccess          (Apache: HTTPS + www canonical + SPA/404 + caching)
        - _redirects         (Netlify)
        - netlify.toml        (Netlify)
        - vercel.json         (Vercel)

Dependency-free (Python 3.6+ stdlib only). Safe to re-run (idempotent).

Usage:
    python3 tools/migrate.py --site site --domain https://your-new-domain.com
    python3 tools/migrate.py --site site --domain https://track.example.com \
        --old-url https://YukiDiablo.github.io/yuki_diablo_public \
        --shim-path /assets/mobile-bridge-shim.js --www-mode strip
"""
import argparse
import os
import re
import shutil
import sys
from datetime import datetime, timezone
from urllib.parse import urlparse

DEFAULT_OLD_URL = "https://YukiDiablo.github.io/yuki_diablo_public"
_HERE = os.path.dirname(os.path.abspath(__file__))


def _find(candidates):
    for c in candidates:
        if os.path.exists(c):
            return c
    return candidates[0]


# Look in a few sensible places so the tool works whether run from the package
# root, from tools/, or copied elsewhere.
SHIM_SRC = _find([
    os.path.join(_HERE, "..", "src", "mobile-bridge-shim.js"),
    os.path.join(_HERE, "src", "mobile-bridge-shim.js"),
    os.path.join(os.getcwd(), "src", "mobile-bridge-shim.js"),
])
APP_404_SRC = _find([
    os.path.join(_HERE, "..", "deploy", "404.html"),
    os.path.join(_HERE, "deploy", "404.html"),
    os.path.join(os.getcwd(), "deploy", "404.html"),
])

HTML_EXTS = (".html", ".htm")
TEXT_EXTS = HTML_EXTS + (".js", ".css", ".json", ".xml", ".webmanifest", ".txt")


def log(msg):
    print(msg, flush=True)


def norm_domain(d):
    d = d.strip().rstrip("/")
    if not re.match(r"^https?://", d):
        d = "https://" + d
    return d


def variants(url):
    """Return http/https + trailing-slash variants of a URL for replacement."""
    u = url.rstrip("/")
    noscheme = re.sub(r"^https?://", "", u)
    out = set()
    for scheme in ("https://", "http://"):
        for tail in ("", "/"):
            out.add(scheme + noscheme + tail)
    out.add(noscheme)
    out.add(noscheme + "/")
    # longest first so we replace the most specific match first
    return sorted(out, key=len, reverse=True)


def inject_shim(html, shim_path):
    if "mobile-bridge-shim" in html:
        return html  # already injected
    tag = '<script src="%s"></script>' % shim_path
    # Prefer to put it as the very first thing in <head>.
    m = re.search(r"<head[^>]*>", html, re.IGNORECASE)
    if m:
        i = m.end()
        return html[:i] + "\n    " + tag + html[i:]
    # No <head>: put before first <script>, else before </body>, else prepend.
    m = re.search(r"<script", html, re.IGNORECASE)
    if m:
        return html[:m.start()] + tag + "\n" + html[m.start():]
    m = re.search(r"</body>", html, re.IGNORECASE)
    if m:
        return html[:m.start()] + tag + "\n" + html[m.start():]
    return tag + "\n" + html


def add_canonical(html, page_url):
    if re.search(r'<link[^>]+rel=["\']canonical["\']', html, re.IGNORECASE):
        return html
    tag = '<link rel="canonical" href="%s">' % page_url
    m = re.search(r"<head[^>]*>", html, re.IGNORECASE)
    if m:
        i = m.end()
        return html[:i] + "\n    " + tag + html[i:]
    return html


def rel_url_for(root, path, domain):
    """Build the canonical absolute URL for a file on the new domain."""
    rel = os.path.relpath(path, root).replace(os.sep, "/")
    if rel.endswith("index.html"):
        rel = rel[: -len("index.html")]
    if rel == ".":
        rel = ""
    return domain + "/" + rel.lstrip("/")


def rewrite_text(text, old_url, domain):
    """Replace old-domain references with the new domain / root-relative paths."""
    changed = text
    old_parsed = urlparse(old_url if "://" in old_url else "https://" + old_url)
    old_base_path = old_parsed.path.rstrip("/")  # e.g. /yuki_diablo_public

    for v in variants(old_url):
        # Absolute old URL -> new domain root
        changed = changed.replace(v, domain + "/")

    # Collapse accidental double slashes in hrefs like domain.com//foo
    changed = re.sub(r'(https?://[^/"\'\s]+)//+', r"\1/", changed)

    # If the project lived under a sub-path, strip leftover sub-path prefixes
    # in root-relative links: /yuki_diablo_public/foo -> /foo
    if old_base_path and old_base_path != "/":
        changed = changed.replace('="' + old_base_path + "/", '="/')
        changed = changed.replace("='" + old_base_path + "/", "='/")
        changed = changed.replace('="' + old_base_path + '"', '="/"')
    return changed


def process_site(site, domain, old_url, shim_path):
    html_pages = []
    n_html = n_text = 0
    for dirpath, _dirs, files in os.walk(site):
        for fn in files:
            fp = os.path.join(dirpath, fn)
            ext = os.path.splitext(fn)[1].lower()
            if ext not in TEXT_EXTS:
                continue
            try:
                with open(fp, "r", encoding="utf-8", errors="replace") as f:
                    content = f.read()
            except Exception as e:
                log("  ! skip (read) %s: %s" % (fp, e))
                continue

            original = content
            content = rewrite_text(content, old_url, domain)
            n_text += 1

            if ext in HTML_EXTS:
                page_url = rel_url_for(site, fp, domain)
                content = inject_shim(content, shim_path)
                content = add_canonical(content, page_url)
                html_pages.append(page_url)
                n_html += 1

            if content != original:
                with open(fp, "w", encoding="utf-8") as f:
                    f.write(content)

    return sorted(set(html_pages)), n_html, n_text


def copy_shim(site, shim_path):
    dest_rel = shim_path.lstrip("/")
    dest = os.path.join(site, dest_rel.replace("/", os.sep))
    os.makedirs(os.path.dirname(dest), exist_ok=True)
    if os.path.exists(SHIM_SRC):
        shutil.copyfile(SHIM_SRC, dest)
        log("  + shim copied -> %s" % os.path.join(site, dest_rel))
    else:
        log("  ! shim source not found at %s (skipping copy)" % SHIM_SRC)


def write_robots(site, domain):
    p = os.path.join(site, "robots.txt")
    with open(p, "w", encoding="utf-8") as f:
        f.write(
            "User-agent: *\n"
            "Allow: /\n\n"
            "Sitemap: %s/sitemap.xml\n" % domain
        )
    log("  + robots.txt")


def write_sitemap(site, domain, pages):
    now = datetime.now(timezone.utc).strftime("%Y-%m-%d")
    lines = ['<?xml version="1.0" encoding="UTF-8"?>',
             '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">']
    if not pages:
        pages = [domain + "/"]
    for url in pages:
        prio = "1.0" if url.rstrip("/") == domain.rstrip("/") else "0.7"
        lines += ["  <url>",
                  "    <loc>%s</loc>" % url,
                  "    <lastmod>%s</lastmod>" % now,
                  "    <changefreq>weekly</changefreq>",
                  "    <priority>%s</priority>" % prio,
                  "  </url>"]
    lines.append("</urlset>\n")
    with open(os.path.join(site, "sitemap.xml"), "w", encoding="utf-8") as f:
        f.write("\n".join(lines))
    log("  + sitemap.xml (%d urls)" % len(pages))


def write_404(site):
    dest = os.path.join(site, "404.html")
    if os.path.exists(dest):
        return
    if os.path.exists(APP_404_SRC):
        shutil.copyfile(APP_404_SRC, dest)
        log("  + 404.html (from app)")


def write_htaccess(site, domain, www_mode):
    host = urlparse(domain).netloc
    bare = host[4:] if host.startswith("www.") else host
    if www_mode == "www":
        canon_host = "www." + bare
        cond = (
            "RewriteCond %{HTTP_HOST} !^www\\. [NC]\n"
            "RewriteRule ^ https://www." + bare + "%{REQUEST_URI} [L,R=301]\n"
        )
    else:  # strip
        canon_host = bare
        cond = (
            "RewriteCond %{HTTP_HOST} ^www\\.(.+)$ [NC]\n"
            "RewriteRule ^ https://%1%{REQUEST_URI} [L,R=301]\n"
        )
    # NOTE: built with plain concatenation on purpose — the content contains
    # literal %{...} tokens that would collide with %-formatting.
    content = (
        "# --- Force HTTPS ---\n"
        "RewriteEngine On\n"
        "RewriteCond %{HTTPS} !=on\n"
        "RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]\n\n"
        "# --- Canonical host (" + canon_host + ") ---\n" + cond + "\n"
        "# --- Pretty 404 ---\n"
        "ErrorDocument 404 /404.html\n\n"
        "# --- Caching for static assets ---\n"
        "<IfModule mod_expires.c>\n"
        "  ExpiresActive On\n"
        "  ExpiresByType text/css \"access plus 1 year\"\n"
        "  ExpiresByType application/javascript \"access plus 1 year\"\n"
        "  ExpiresByType image/png \"access plus 1 year\"\n"
        "  ExpiresByType image/jpeg \"access plus 1 year\"\n"
        "  ExpiresByType image/webp \"access plus 1 year\"\n"
        "  ExpiresByType image/svg+xml \"access plus 1 year\"\n"
        "  ExpiresByType text/html \"access plus 0 seconds\"\n"
        "</IfModule>\n\n"
        "# --- Gzip ---\n"
        "<IfModule mod_deflate.c>\n"
        "  AddOutputFilterByType DEFLATE text/html text/css application/javascript "
        "application/json image/svg+xml\n"
        "</IfModule>\n"
    )
    with open(os.path.join(site, ".htaccess"), "w", encoding="utf-8") as f:
        f.write(content)
    log("  + .htaccess (canonical host: " + canon_host + ")")


def write_netlify(site, domain):
    with open(os.path.join(site, "_redirects"), "w", encoding="utf-8") as f:
        f.write("# Netlify redirects\n/*    /404.html   404\n")
    with open(os.path.join(site, "netlify.toml"), "w", encoding="utf-8") as f:
        f.write(
            "[build]\n  publish = \".\"\n\n"
            "[[headers]]\n  for = \"/*\"\n  [headers.values]\n"
            "    X-Content-Type-Options = \"nosniff\"\n"
            "    X-Frame-Options = \"SAMEORIGIN\"\n"
            "    Referrer-Policy = \"strict-origin-when-cross-origin\"\n"
        )
    log("  + _redirects + netlify.toml")


def write_vercel(site):
    with open(os.path.join(site, "vercel.json"), "w", encoding="utf-8") as f:
        f.write(
            '{\n'
            '  "cleanUrls": true,\n'
            '  "trailingSlash": false,\n'
            '  "headers": [\n'
            '    {\n'
            '      "source": "/(.*)",\n'
            '      "headers": [\n'
            '        { "key": "X-Content-Type-Options", "value": "nosniff" },\n'
            '        { "key": "X-Frame-Options", "value": "SAMEORIGIN" },\n'
            '        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }\n'
            '      ]\n'
            '    }\n'
            '  ]\n'
            '}\n'
        )
    log("  + vercel.json")


def main():
    ap = argparse.ArgumentParser(description="Migrate ZyrexTrack site to a new domain (web-only).")
    ap.add_argument("--site", required=True, help="Path to the mirrored site folder")
    ap.add_argument("--domain", required=True, help="New domain, e.g. https://track.example.com")
    ap.add_argument("--old-url", default=DEFAULT_OLD_URL, help="Old source URL to rewrite")
    ap.add_argument("--shim-path", default="/assets/mobile-bridge-shim.js",
                    help="Where to place & reference the bridge shim")
    ap.add_argument("--www-mode", choices=["strip", "www"], default="strip",
                    help="Canonical host style for .htaccess (default: strip www)")
    args = ap.parse_args()

    site = args.site
    if not os.path.isdir(site):
        log("ERROR: site folder not found: %s" % site)
        sys.exit(1)
    domain = norm_domain(args.domain)
    old_url = args.old_url

    log("=" * 62)
    log(" ZyrexTrack -> web-only migration")
    log("  site   : %s" % os.path.abspath(site))
    log("  domain : %s" % domain)
    log("  old url: %s" % old_url)
    log("=" * 62)

    log("[1/3] Copying bridge shim…")
    copy_shim(site, args.shim_path)

    log("[2/3] Rewriting URLs + injecting shim/canonical…")
    pages, n_html, n_text = process_site(site, domain, old_url, args.shim_path)
    log("      processed %d text files, %d HTML pages" % (n_text, n_html))

    log("[3/3] Generating SEO + deploy files…")
    write_404(site)
    write_robots(site, domain)
    write_sitemap(site, domain, pages)
    write_htaccess(site, domain, args.www_mode)
    write_netlify(site, domain)
    write_vercel(site)

    log("")
    log("[✓] Done. '%s/' is ready to upload to your new domain." % site)
    log("    Apache/cPanel : upload the folder contents to public_html/")
    log("    Netlify/Vercel: deploy the folder as-is")


if __name__ == "__main__":
    main()
