"""
DiscordBooster — HTML → PHP Converter + Font Replacer
=====================================================
Two tools in one GUI:

TOOL 1 — HTML → PHP CONVERSION
  Converts self-contained HTML service pages into PHP pages that use
  shared header.php / footer.php includes.  For each HTML file the tool:
    1. Extracts meta tags  (title, description, keywords, canonical,
                            og:title, og:description, og:image, schema JSON)
    2. Auto-detects the active platform from the filename
    3. Removes the old standalone nav  (aria-label="Main navigation" / id="site-nav")
    4. Removes the mobile drawer       (#mobile-drawer)
    5. Removes inline nav JavaScript   (toggleMega / toggleSimple blocks)
    6. Removes the old footer          (<footer> element)
    7. Replaces Playfair Display font → Sora (+ weight 900→800)
    8. Keeps every page-specific <style>, section, FAQ, scripts, etc.
    9. Writes a .php file with PHP variable declarations + include calls

TOOL 2 — FONT REPLACEMENT (IN-PLACE)
  Batch-replaces Playfair Display → Sora in existing HTML files without
  converting to PHP.  Changes applied in-place:
    • font-family: 'Playfair Display', serif  →  'Sora', sans-serif
    • Google Fonts <link> tag updated to load Sora instead
    • font-weight: 900  →  font-weight: 800  (Sora's heaviest weight)

Packaging as an EXE (Windows):
  pip install pyinstaller
  pyinstaller --onefile --windowed --icon=icon.ico html_to_php_converter.py
  → dist/html_to_php_converter.exe

Dependencies:  beautifulsoup4  lxml  (both pure-Python-compatible)
  pip install beautifulsoup4 lxml
"""

import os
import re
import sys
import json
import queue
import threading
import textwrap
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, ttk
from pathlib import Path
from bs4 import BeautifulSoup, Comment

# ─────────────────────────────────────────────────────────────────────────────
# PLATFORM DETECTION
# ─────────────────────────────────────────────────────────────────────────────

PLATFORM_KEYWORDS = {
    "instagram": ["instagram"],
    "tiktok":    ["tiktok"],
    "youtube":   ["youtube"],
    "twitter":   ["twitter", "-x-"],
    "facebook":  ["facebook"],
    "reddit":    ["reddit"],
    "spotify":   ["spotify"],
    "telegram":  ["telegram"],
    "twitch":    ["twitch"],
    "threads":   ["threads"],
    "discord":   ["discord"],
}

def detect_platform(filename: str) -> str:
    """Infer active_platform from the filename (e.g. buy-instagram-followers → instagram)."""
    name = filename.lower()
    for platform, keywords in PLATFORM_KEYWORDS.items():
        if any(kw in name for kw in keywords):
            return platform
    return ""


# ─────────────────────────────────────────────────────────────────────────────
# FONT REPLACEMENT
# ─────────────────────────────────────────────────────────────────────────────

# Old → New mapping.  Edit these two blocks to change the font swap.
OLD_FONT_LINK_FRAGMENT = "Playfair+Display"
NEW_FONT_LINK = (
    '<link href="https://fonts.googleapis.com/css2'
    '?family=Sora:wght@500;600;700;800'
    '&family=Outfit:wght@300;400;500;600;700'
    '&display=swap" rel="stylesheet">'
)


def replace_fonts(text: str) -> str:
    """
    Replace all Playfair Display references with Sora in a string of
    HTML/CSS/PHP content.  Handles:
      - font-family declarations (with/without spaces, with/without serif fallback)
      - Google Fonts <link> tag
      - font-weight: 900 → 800  (Sora's heaviest available weight)
    """
    # 1. font-family: 'Playfair Display',serif  → 'Sora', sans-serif
    text = re.sub(
        r"'Playfair Display'\s*,\s*serif",
        "'Sora', sans-serif",
        text
    )
    text = text.replace("'Playfair Display'", "'Sora'")
    text = re.sub(
        r'"Playfair Display"\s*,\s*serif',
        '"Sora", sans-serif',
        text
    )
    text = text.replace('"Playfair Display"', '"Sora"')

    # 2. Google Fonts <link> — replace entire tag if it contains the old font
    text = re.sub(
        r'<link\s+href="https://fonts\.googleapis\.com/css2\?[^"]*'
        + re.escape(OLD_FONT_LINK_FRAGMENT)
        + r'[^"]*"\s+rel="stylesheet"\s*/?>',
        NEW_FONT_LINK,
        text
    )
    text = re.sub(
        r'<link\s+rel="stylesheet"\s+href="https://fonts\.googleapis\.com/css2\?[^"]*'
        + re.escape(OLD_FONT_LINK_FRAGMENT)
        + r'[^"]*"\s*/?>',
        NEW_FONT_LINK,
        text
    )

    # 3. font-weight:900 → 800  (Sora maxes at 800)
    text = re.sub(r'font-weight\s*:\s*900', 'font-weight: 800', text)

    return text


# ─────────────────────────────────────────────────────────────────────────────
# HTML → PHP CONVERSION LOGIC
# ─────────────────────────────────────────────────────────────────────────────

# Script content patterns that belong to the nav (not to page logic)
NAV_SCRIPT_MARKERS = [
    "toggleMega",
    "toggleSimple",
    "toggleMobileMenu",
    "closeAll()",
    "nav-hamburger",
    "mobile-drawer",
]

def is_nav_script(script_tag) -> bool:
    """Return True if a <script> block is the nav JS (should be removed)."""
    text = script_tag.get_text()
    return any(marker in text for marker in NAV_SCRIPT_MARKERS)


def extract_meta(soup: BeautifulSoup) -> dict:
    """Pull all the PHP variables we need from <head>."""
    def _meta_content(attrs):
        tag = soup.find("meta", attrs=attrs)
        return tag.get("content", "").strip() if tag else ""

    def _link_href(attrs):
        tag = soup.find("link", attrs=attrs)
        return tag.get("href", "").strip() if tag else ""

    title_tag = soup.find("title")
    page_title = title_tag.get_text().strip() if title_tag else ""

    # Schema JSON-LD
    schema_json = ""
    schema_tag = soup.find("script", attrs={"type": "application/ld+json"})
    if schema_tag:
        raw = schema_tag.get_text().strip()
        try:                                    # pretty-print it
            schema_json = json.dumps(json.loads(raw), ensure_ascii=False, indent=2)
        except json.JSONDecodeError:
            schema_json = raw

    return {
        "page_title":       page_title,
        "meta_description": _meta_content({"name": "description"}),
        "meta_keywords":    _meta_content({"name": "keywords"}),
        "canonical_url":    _link_href({"rel": "canonical"}),
        "og_title":         _meta_content({"property": "og:title"}),
        "og_description":   _meta_content({"property": "og:description"}),
        "og_image":         _meta_content({"property": "og:image"}),
        "schema_json":      schema_json,
    }


def collect_head_styles(soup: BeautifulSoup) -> list[str]:
    """Grab all <style> blocks from <head> — these are page-specific styles."""
    if not soup.head:
        return []
    return [str(tag) for tag in soup.head.find_all("style")]


def clean_body(soup: BeautifulSoup) -> str:
    """
    Remove nav / drawer / nav-JS / footer from <body> and return
    the remaining inner HTML as a string.
    """
    body = soup.body
    if not body:
        return ""

    # 1. Main site nav  (old pages use <nav>, new header.php uses <header id="site-nav">)
    for selector, attrs in [
        ("header", {"id": "site-nav"}),
        ("nav",    {"aria-label": "Main navigation"}),
        ("nav",    {"id": "site-nav"}),
    ]:
        el = body.find(selector, attrs=attrs)
        if el:
            el.decompose()
            break  # only one main nav per page

    # 2. Mobile drawer
    drawer = body.find(id="mobile-drawer")
    if drawer:
        drawer.decompose()

    # 3. Nav JS <script> block
    for script in body.find_all("script"):
        if is_nav_script(script):
            script.decompose()

    # 4. HTML comments that are just nav separators  (<!-- ══ NAV JS ... ══ -->)
    for comment in body.find_all(string=lambda t: isinstance(t, Comment)):
        if "NAV" in comment.upper() or "MOBILE" in comment.upper():
            comment.extract()

    # 5. Footer element
    footer = body.find("footer")
    if footer:
        footer.decompose()

    return body.decode_contents()


def php_escape(value: str) -> str:
    """Escape a string for a PHP double-quoted string."""
    return value.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$")


def build_php(meta: dict, platform: str, head_styles: list[str],
              body_html: str, include_path: str) -> str:
    """
    Assemble the final .php file content.

    include_path is the relative path prefix used in include statements,
    e.g. "" (same folder), "../" (one level up), "includes/" etc.
    """
    inc = include_path.rstrip("/")
    header_inc = f"{inc}/header.php" if inc else "header.php"
    footer_inc = f"{inc}/footer.php" if inc else "footer.php"

    # ── PHP variable block ───────────────────────────────────────────────
    schema = meta["schema_json"]
    if schema:
        # Use nowdoc (no variable interpolation) so $ signs are safe
        schema_block = (
            "$schema_json = <<<'JSON'\n"
            + schema + "\n"
            + "JSON;\n"
        )
    else:
        schema_block = "// $schema_json = '';  // add Schema.org JSON-LD here if needed\n"

    php_vars = textwrap.dedent(f"""\
        <?php
        /* ═══════════════════════════════════════════════════════
           PAGE META  —  edit these values for each page
        ═══════════════════════════════════════════════════════ */
        $page_title       = "{php_escape(meta['page_title'])}";
        $meta_description = "{php_escape(meta['meta_description'])}";
        $meta_keywords    = "{php_escape(meta['meta_keywords'])}";
        $canonical_url    = "{php_escape(meta['canonical_url'])}";
        $og_title         = "{php_escape(meta['og_title'])}";
        $og_description   = "{php_escape(meta['og_description'])}";
        $og_image         = "{php_escape(meta['og_image'])}";
        $active_platform  = "{platform}";

        {schema_block}
        include '{header_inc}';
        ?>
    """)

    # ── Page-specific <style> blocks ─────────────────────────────────────
    styles_block = ""
    if head_styles:
        styles_block = (
            "\n<!-- ══ PAGE-SPECIFIC STYLES ══════════════════════════════\n"
            "     Review: remove any :root / nav / footer / body / font\n"
            "     rules already provided by header.php / footer.php\n"
            "══════════════════════════════════════════════════════ -->\n"
            + "\n".join(head_styles)
            + "\n"
        )

    # ── Footer include ────────────────────────────────────────────────────
    footer_block = f"\n<?php include '{footer_inc}'; ?>\n"

    # ── Strip .html / .htm extensions from all internal href links ──────
    # Converts href="buy-instagram-followers.html" → href="buy-instagram-followers"
    # Preserves anchors:  page.html#section → page#section
    # Preserves external URLs that happen to contain .html (no false positives
    # because we only match the extension immediately before " ' # ? or space)
    body_html = re.sub(
        r'(href=["\'])([^"\']*?)\.html(["\'#?\s])',
        r'\1\2\3',
        body_html
    )
    body_html = re.sub(
        r'(href=["\'])([^"\']*?)\.htm(["\'#?\s])',
        r'\1\2\3',
        body_html
    )

    # ── Replace old fonts with new fonts ─────────────────────────────────
    full_output = php_vars + styles_block + "\n" + body_html.strip() + "\n" + footer_block
    full_output = replace_fonts(full_output)

    return full_output


def convert_file(html_path: Path, out_dir: Path,
                 include_path: str) -> tuple[bool, str]:
    """
    Convert a single HTML file to PHP.
    Returns (success: bool, message: str).
    """
    try:
        html_content = html_path.read_text(encoding="utf-8", errors="replace")
    except OSError as e:
        return False, f"Cannot read file: {e}"

    try:
        soup = BeautifulSoup(html_content, "lxml")
    except Exception as e:
        return False, f"Parse error: {e}"

    meta         = extract_meta(soup)
    platform     = detect_platform(html_path.name)
    head_styles  = collect_head_styles(soup)
    body_html    = clean_body(soup)
    php_content  = build_php(meta, platform, head_styles, body_html, include_path)

    out_path = out_dir / (html_path.stem + ".php")
    try:
        out_path.write_text(php_content, encoding="utf-8")
    except OSError as e:
        return False, f"Cannot write output: {e}"

    return True, f"✓  {html_path.name}  →  {out_path.name}"


# ─────────────────────────────────────────────────────────────────────────────
# GUI
# ─────────────────────────────────────────────────────────────────────────────

APP_TITLE   = "DiscordBooster — HTML → PHP Converter + Font Replacer"
APP_VERSION = "1.1.0"
BG          = "#faf8f4"
INK         = "#1a1612"
ACCENT      = "#ff4500"
MUTED       = "#8c8278"
SURFACE     = "#ffffff"
BORDER      = "#e8e2d8"
GREEN       = "#1a7a4a"
RED_ERR     = "#cc0000"
FONT_BODY   = ("Segoe UI", 10)
FONT_MONO   = ("Consolas", 9)
FONT_HEAD   = ("Segoe UI Semibold", 11)
FONT_TITLE  = ("Segoe UI Bold", 14)


class ConverterApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title(APP_TITLE)
        self.resizable(True, True)
        self.minsize(820, 620)
        self.configure(bg=BG)

        # State
        self.html_files: list[Path] = []
        self.out_dir:    Path | None = None
        self.log_queue  = queue.Queue()
        self.converting = False

        self._build_ui()
        self._poll_log()

        # Centre on screen
        self.update_idletasks()
        w, h = 900, 680
        x = (self.winfo_screenwidth()  - w) // 2
        y = (self.winfo_screenheight() - h) // 2
        self.geometry(f"{w}x{h}+{x}+{y}")

    # ── UI construction ───────────────────────────────────────────────────

    def _build_ui(self):
        # ── Title bar ──
        title_bar = tk.Frame(self, bg=INK, pady=0)
        title_bar.pack(fill="x")
        tk.Label(
            title_bar, text=f"  Discord", font=("Segoe UI Bold", 13),
            fg="#ff4500", bg=INK
        ).pack(side="left", pady=12)
        tk.Label(
            title_bar, text=f"Booster  HTML → PHP Converter + Font Replacer  v{APP_VERSION}",
            font=("Segoe UI", 13), fg="white", bg=INK
        ).pack(side="left", pady=12)

        # ── Main container ──
        main = tk.Frame(self, bg=BG)
        main.pack(fill="both", expand=True, padx=18, pady=14)

        # ── Left column (settings) ──
        left = tk.Frame(main, bg=BG, width=310)
        left.pack(side="left", fill="y", padx=(0, 14))
        left.pack_propagate(False)

        self._section_label(left, "1 · SELECT HTML FILES")
        self.files_listbox = tk.Listbox(
            left, bg=SURFACE, fg=INK, font=FONT_MONO,
            selectmode="extended", relief="flat",
            highlightthickness=1, highlightbackground=BORDER,
            selectbackground=ACCENT, selectforeground="white",
            height=9
        )
        self.files_listbox.pack(fill="x", pady=(4, 0))

        btn_row1 = tk.Frame(left, bg=BG)
        btn_row1.pack(fill="x", pady=(5, 0))
        self._btn(btn_row1, "＋ Add Files",    self._add_files,    primary=True ).pack(side="left", padx=(0, 4))
        self._btn(btn_row1, "＋ Add Folder",   self._add_folder                ).pack(side="left", padx=(0, 4))
        self._btn(btn_row1, "✕ Remove Sel.",   self._remove_selected            ).pack(side="left")
        self._btn(btn_row1, "Clear All",        self._clear_files               ).pack(side="right")

        tk.Label(left, text=f"0 files selected", font=FONT_BODY,
                 bg=BG, fg=MUTED).pack(anchor="w", pady=(3,0))
        self.file_count_var = tk.StringVar(value="0 files selected")
        tk.Label(left, textvariable=self.file_count_var,
                 font=FONT_BODY, bg=BG, fg=MUTED).pack(anchor="w")

        tk.Frame(left, bg=BORDER, height=1).pack(fill="x", pady=10)

        self._section_label(left, "2 · OUTPUT FOLDER")
        self.out_dir_var = tk.StringVar(value="Same folder as each input file")
        out_frame = tk.Frame(left, bg=SURFACE,
                             highlightthickness=1, highlightbackground=BORDER)
        out_frame.pack(fill="x")
        tk.Label(out_frame, textvariable=self.out_dir_var,
                 font=FONT_MONO, bg=SURFACE, fg=MUTED,
                 anchor="w", wraplength=220, justify="left",
                 padx=6, pady=5).pack(side="left", fill="both", expand=True)
        self._btn(out_frame, "Browse", self._choose_out_dir,
                  primary=True).pack(side="right", padx=4, pady=4)
        self._btn_txt = tk.StringVar(value="Same as input")
        self.same_dir_chk_var = tk.BooleanVar(value=True)
        tk.Checkbutton(
            left, text="Same folder as input file  (one PHP beside each HTML)",
            variable=self.same_dir_chk_var, bg=BG, fg=INK,
            font=FONT_BODY, activebackground=BG,
            command=self._toggle_same_dir
        ).pack(anchor="w", pady=(5,0))

        tk.Frame(left, bg=BORDER, height=1).pack(fill="x", pady=10)

        self._section_label(left, "3 · INCLUDE PATH")
        tk.Label(
            left,
            text="Relative path to header.php / footer.php from\n"
                 "the PHP file location. Leave blank if they're\n"
                 "in the same directory.",
            font=("Segoe UI", 9), bg=BG, fg=MUTED, justify="left"
        ).pack(anchor="w")
        self.include_path_var = tk.StringVar(value="")
        inc_entry = tk.Entry(
            left, textvariable=self.include_path_var,
            font=FONT_MONO, bg=SURFACE, fg=INK, relief="flat",
            highlightthickness=1, highlightbackground=BORDER, bd=4
        )
        inc_entry.pack(fill="x", pady=(4,0))
        tk.Label(left,
                 text='Examples:  ""  →  same dir\n'
                      '"../"  →  one level up\n'
                      '"includes/"  →  subfolder',
                 font=("Segoe UI", 8), bg=BG, fg=MUTED,
                 justify="left").pack(anchor="w", pady=(2,0))

        tk.Frame(left, bg=BORDER, height=1).pack(fill="x", pady=10)

        # Convert button
        self.convert_btn = self._btn(
            left, "▶  Convert Files", self._start_conversion,
            primary=True, large=True
        )
        self.convert_btn.pack(fill="x", pady=(0,6))
        self.progress = ttk.Progressbar(left, mode="determinate")
        self.progress.pack(fill="x")
        self.status_var = tk.StringVar(value="Ready — add HTML files to begin.")
        tk.Label(left, textvariable=self.status_var,
                 font=FONT_BODY, bg=BG, fg=MUTED,
                 wraplength=280, justify="left").pack(anchor="w", pady=(5,0))

        tk.Frame(left, bg=BORDER, height=1).pack(fill="x", pady=10)

        # Font replacement button — replaces Playfair Display → Sora in HTML files
        self._section_label(left, "4 · REPLACE FONT (HTML FILES)")
        tk.Label(
            left,
            text="Replace Playfair Display → Sora in all\n"
                 "loaded HTML files (in-place). Also updates\n"
                 "the Google Fonts <link> and font-weight.",
            font=("Segoe UI", 9), bg=BG, fg=MUTED, justify="left"
        ).pack(anchor="w")
        self.font_btn = self._btn(
            left, "🔤  Replace Font in HTML Files", self._start_font_replace,
            primary=False, large=True
        )
        self.font_btn.pack(fill="x", pady=(6, 0))

        # ── Right column (log) ──
        right = tk.Frame(main, bg=BG)
        right.pack(side="left", fill="both", expand=True)

        log_header = tk.Frame(right, bg=BG)
        log_header.pack(fill="x", pady=(0,4))
        self._section_label(log_header, "CONVERSION LOG").pack(side="left")
        self._btn(log_header, "Clear log", self._clear_log).pack(side="right")
        self._btn(log_header, "Copy log",  self._copy_log ).pack(side="right", padx=(0,4))

        self.log = scrolledtext.ScrolledText(
            right, font=FONT_MONO, bg="#1a1612", fg="#e8e2d8",
            insertbackground="white", relief="flat",
            state="disabled", wrap="word",
            highlightthickness=1, highlightbackground=BORDER
        )
        self.log.pack(fill="both", expand=True)
        self.log.tag_config("ok",      foreground="#4ec880")
        self.log.tag_config("error",   foreground="#ff6b6b")
        self.log.tag_config("warn",    foreground="#f5a623")
        self.log.tag_config("header",  foreground="#69c9d0")
        self.log.tag_config("muted",   foreground="#8c8278")
        self.log.tag_config("accent",  foreground="#ff4500")

        self._log("header", f"DiscordBooster HTML → PHP Converter + Font Replacer  v{APP_VERSION}\n")
        self._log("muted",  "────────────────────────────────────────────────\n")
        self._log("muted",  "How to use:\n"
                            "  1. Add your HTML files or an entire folder\n"
                            "  2. Choose where to save the PHP files\n"
                            "  3. Set the include path (usually leave blank)\n"
                            "  4. Click Convert Files\n\n"
                            "Font Replacement (HTML files in-place):\n"
                            "  1. Add your HTML files\n"
                            "  2. Click 'Replace Font in HTML Files'\n"
                            "  → Replaces Playfair Display → Sora\n"
                            "  → Updates Google Fonts <link> tag\n"
                            "  → Fixes font-weight: 900 → 800\n\n"
                            "Each HTML page becomes a .php file with:\n"
                            "  • PHP variables for all meta tags\n"
                            "  • <?php include 'header.php'; ?>\n"
                            "  • Your page content (styles, sections, JS)\n"
                            "  • <?php include 'footer.php'; ?>\n\n")

    def _section_label(self, parent, text):
        lbl = tk.Label(parent, text=text, font=("Segoe UI Bold", 9),
                       bg=BG, fg=ACCENT, anchor="w")
        lbl.pack(anchor="w", pady=(0,2))
        return lbl

    def _btn(self, parent, text, command, primary=False, large=False):
        fg   = "white" if primary else INK
        bg   = ACCENT  if primary else SURFACE
        hbg  = "#cc3700" if primary else "#e8e2d8"
        font = ("Segoe UI Semibold", 11) if large else ("Segoe UI", 9)
        pad  = (10, 8) if large else (6, 4)
        btn  = tk.Button(
            parent, text=text, command=command,
            fg=fg, bg=bg, font=font,
            relief="flat", cursor="hand2",
            padx=pad[0], pady=pad[1],
            activebackground=hbg, activeforeground=fg,
            bd=0
        )
        btn.bind("<Enter>", lambda e: btn.config(bg=hbg))
        btn.bind("<Leave>", lambda e: btn.config(bg=bg))
        return btn

    # ── File management ───────────────────────────────────────────────────

    def _add_files(self):
        paths = filedialog.askopenfilenames(
            title="Select HTML files",
            filetypes=[("HTML files", "*.html *.htm"), ("All files", "*.*")]
        )
        added = 0
        for p in paths:
            path = Path(p)
            if path not in self.html_files:
                self.html_files.append(path)
                self.files_listbox.insert("end", path.name)
                added += 1
        if added:
            self._update_file_count()
            self._log("ok", f"Added {added} file(s).\n")

    def _add_folder(self):
        folder = filedialog.askdirectory(title="Select folder containing HTML files")
        if not folder:
            return
        folder = Path(folder)
        added = 0
        for path in sorted(folder.glob("*.html")):
            if path not in self.html_files:
                self.html_files.append(path)
                self.files_listbox.insert("end", path.name)
                added += 1
        for path in sorted(folder.glob("*.htm")):
            if path not in self.html_files:
                self.html_files.append(path)
                self.files_listbox.insert("end", path.name)
                added += 1
        if added:
            self._update_file_count()
            self._log("ok", f"Added {added} file(s) from {folder.name}/\n")
        else:
            self._log("warn", f"No .html files found in {folder.name}/\n")

    def _remove_selected(self):
        sel = list(self.files_listbox.curselection())
        for idx in reversed(sel):
            self.files_listbox.delete(idx)
            del self.html_files[idx]
        self._update_file_count()

    def _clear_files(self):
        self.html_files.clear()
        self.files_listbox.delete(0, "end")
        self._update_file_count()

    def _update_file_count(self):
        n = len(self.html_files)
        self.file_count_var.set(f"{n} file{'s' if n != 1 else ''} selected")

    # ── Output directory ──────────────────────────────────────────────────

    def _choose_out_dir(self):
        folder = filedialog.askdirectory(title="Select output folder for PHP files")
        if folder:
            self.out_dir = Path(folder)
            self.out_dir_var.set(str(self.out_dir))
            self.same_dir_chk_var.set(False)
            self._log("muted", f"Output folder: {self.out_dir}\n")

    def _toggle_same_dir(self):
        if self.same_dir_chk_var.get():
            self.out_dir = None
            self.out_dir_var.set("Same folder as each input file")

    # ── Log helpers ───────────────────────────────────────────────────────

    def _log(self, tag: str, text: str):
        self.log.config(state="normal")
        self.log.insert("end", text, tag)
        self.log.see("end")
        self.log.config(state="disabled")

    def _poll_log(self):
        try:
            while True:
                tag, text = self.log_queue.get_nowait()
                self._log(tag, text)
        except queue.Empty:
            pass
        self.after(80, self._poll_log)

    def _clear_log(self):
        self.log.config(state="normal")
        self.log.delete("1.0", "end")
        self.log.config(state="disabled")

    def _copy_log(self):
        text = self.log.get("1.0", "end")
        self.clipboard_clear()
        self.clipboard_append(text)
        self._log("muted", "(log copied to clipboard)\n")

    # ── Conversion ────────────────────────────────────────────────────────

    def _start_conversion(self):
        if self.converting:
            return
        if not self.html_files:
            messagebox.showwarning("No files", "Add at least one HTML file first.")
            return
        self.converting = True
        self.convert_btn.config(state="disabled", text="Converting…")
        self.progress["maximum"] = len(self.html_files)
        self.progress["value"] = 0
        self.status_var.set("Converting…")

        include_path = self.include_path_var.get().strip()

        # Thread so the GUI stays responsive
        t = threading.Thread(
            target=self._run_conversion,
            args=(list(self.html_files), self.out_dir, include_path),
            daemon=True
        )
        t.start()

    def _run_conversion(self, files: list[Path], out_dir: Path | None, include_path: str):
        total = len(files)
        ok = 0
        errors = 0

        self.log_queue.put(("header",
            f"\n{'═'*50}\n"
            f"  Converting {total} file(s)\n"
            f"{'═'*50}\n"
        ))

        for i, html_path in enumerate(files, 1):
            dest = out_dir if out_dir else html_path.parent
            dest.mkdir(parents=True, exist_ok=True)

            self.log_queue.put(("muted", f"  [{i}/{total}]  {html_path.name}  "))
            success, msg = convert_file(html_path, dest, include_path)

            if success:
                ok += 1
                self.log_queue.put(("ok", "✓\n"))
            else:
                errors += 1
                self.log_queue.put(("error", f"✗  {msg}\n"))

            # Update progress on main thread via after()
            self.after(0, self._tick_progress, i)

        # Summary
        self.log_queue.put(("header",
            f"\n{'─'*50}\n"
            f"  Done.  {ok} converted"
            + (f"  |  {errors} error(s)" if errors else "")
            + "\n"
            f"{'─'*50}\n"
        ))
        if ok:
            self.log_queue.put(("ok",
                f"\n  ✔ {ok} PHP file(s) saved.\n"
                "  Review the <style> block in each file — you can remove\n"
                "  any :root / nav / footer / body rules that are now\n"
                "  provided by header.php and footer.php.\n\n"
            ))

        self.after(0, self._finish_conversion, ok, errors)

    def _tick_progress(self, value: int):
        self.progress["value"] = value

    def _finish_conversion(self, ok: int, errors: int):
        self.converting = False
        self.convert_btn.config(state="normal", text="▶  Convert Files")
        if errors:
            self.status_var.set(f"Done — {ok} OK, {errors} error(s). Check log.")
        else:
            self.status_var.set(f"Done — {ok} file(s) converted successfully. ✓")

    # ── Font Replacement (in-place on HTML files) ─────────────────────────

    def _start_font_replace(self):
        if self.converting:
            return
        if not self.html_files:
            messagebox.showwarning("No files", "Add at least one HTML file first.")
            return
        confirm = messagebox.askyesno(
            "Replace Font",
            f"This will MODIFY {len(self.html_files)} HTML file(s) in-place,\n"
            "replacing Playfair Display → Sora.\n\n"
            "Changes include:\n"
            "  • font-family declarations\n"
            "  • Google Fonts <link> tag\n"
            "  • font-weight: 900 → 800\n\n"
            "Proceed?"
        )
        if not confirm:
            return

        self.converting = True
        self.font_btn.config(state="disabled", text="Replacing…")
        self.progress["maximum"] = len(self.html_files)
        self.progress["value"] = 0
        self.status_var.set("Replacing fonts…")

        t = threading.Thread(
            target=self._run_font_replace,
            args=(list(self.html_files),),
            daemon=True
        )
        t.start()

    def _run_font_replace(self, files: list):
        total = len(files)
        ok = 0
        errors = 0

        self.log_queue.put(("header",
            f"\n{'═'*50}\n"
            f"  Font Replace: {total} file(s)\n"
            f"  Playfair Display → Sora\n"
            f"{'═'*50}\n"
        ))

        for i, html_path in enumerate(files, 1):
            self.log_queue.put(("muted", f"  [{i}/{total}]  {html_path.name}  "))
            try:
                content = html_path.read_text(encoding="utf-8", errors="replace")

                # Count how many replacements will happen
                old_count = content.count("Playfair Display") + content.count("Playfair+Display")
                weight_count = len(re.findall(r'font-weight\s*:\s*900', content))

                new_content = replace_fonts(content)

                if new_content != content:
                    html_path.write_text(new_content, encoding="utf-8")
                    ok += 1
                    changes = []
                    if old_count:
                        changes.append(f"{old_count} font refs")
                    if weight_count:
                        changes.append(f"{weight_count} weight fixes")
                    detail = " (" + ", ".join(changes) + ")" if changes else ""
                    self.log_queue.put(("ok", f"✓ updated{detail}\n"))
                else:
                    self.log_queue.put(("warn", "— no Playfair found, skipped\n"))

            except Exception as e:
                errors += 1
                self.log_queue.put(("error", f"✗  {e}\n"))

            self.after(0, self._tick_progress, i)

        self.log_queue.put(("header",
            f"\n{'─'*50}\n"
            f"  Done.  {ok} file(s) updated"
            + (f"  |  {errors} error(s)" if errors else "")
            + f"  |  {total - ok - errors} unchanged"
            + "\n"
            f"{'─'*50}\n"
        ))

        self.after(0, self._finish_font_replace, ok, errors, total)

    def _finish_font_replace(self, ok: int, errors: int, total: int):
        self.converting = False
        self.font_btn.config(state="normal", text="🔤  Replace Font in HTML Files")
        unchanged = total - ok - errors
        parts = []
        if ok:
            parts.append(f"{ok} updated")
        if unchanged:
            parts.append(f"{unchanged} unchanged")
        if errors:
            parts.append(f"{errors} error(s)")
        self.status_var.set(f"Font replace done — {', '.join(parts)}. ✓")


# ─────────────────────────────────────────────────────────────────────────────
# ENTRY POINT
# ─────────────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    app = ConverterApp()
    app.mainloop()
