from __future__ import annotations

from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
from urllib.parse import unquote, urlparse, quote
from email.utils import formatdate
from datetime import datetime
import mimetypes
import os
import json

HOST = "0.0.0.0"
PORT = 8765

BASE_DIR = Path(__file__).resolve().parent
APPS_DIR = BASE_DIR / "apps"
PLUGINS_DIR = BASE_DIR / "plugins"
LOG_DIR = BASE_DIR / "logs"
LOG_FILE = LOG_DIR / "server.log"

LOG_DIR.mkdir(parents=True, exist_ok=True)
APPS_DIR.mkdir(parents=True, exist_ok=True)
PLUGINS_DIR.mkdir(parents=True, exist_ok=True)
os.chdir(BASE_DIR)

# Files considered valid update payloads.
ALLOWED_UPDATE_EXTENSIONS = {
    ".exe",
    ".msi",
    ".msix",
    ".msixbundle",
    ".zip",
}

def is_plugin_file(path: Path) -> bool:
    """Plugin folders accept any file extension, while hiding folder metadata."""
    return path.is_file() and path.name.casefold() != "info.txt" and not path.name.startswith(".")


def creation_timestamp(path: Path) -> float:
    """
    Windows: st_ctime is file creation time.
    The server also provides Last-Modified as a fallback.
    """
    return path.stat().st_ctime


def resolve_app_path(app_name: str) -> Path | None:
    """
    Resolve an application route below apps/ safely and case-insensitively.

    Supports nested edition folders, for example:
        /latest/vimpulsetechtoolkit/Pro
        /latest/vimpulsetechtoolkit/Business
    """
    raw_parts = [
        part.strip()
        for part in app_name.replace("\\", "/").split("/")
        if part.strip()
    ]

    if not raw_parts:
        return None

    current = APPS_DIR

    for raw_part in raw_parts:
        if raw_part in {".", ".."}:
            return None

        wanted = raw_part.casefold()
        match = None

        try:
            for child in current.iterdir():
                if child.is_dir() and child.name.casefold() == wanted:
                    match = child
                    break
        except OSError:
            return None

        if match is None:
            return None

        current = match

    try:
        current.resolve().relative_to(APPS_DIR.resolve())
    except Exception:
        return None

    return current


def find_latest_update(app_name: str) -> Path | None:
    """
    Find the newest valid update payload in:
        apps/<APP OR APP/EDITION>/updates/

    The filename can be anything.

    Examples:
        StoryCutV2.exe
        StoryCut New.exe
        Toolkit Pro 3.1.exe
        Business September Build.msi

    The newest file by creation timestamp wins.
    """
    app_folder = resolve_app_path(app_name)
    if app_folder is None:
        return None

    updates_dir = app_folder / "updates"
    if not updates_dir.exists() or not updates_dir.is_dir():
        return None

    candidates = []

    for path in updates_dir.iterdir():
        if not path.is_file():
            continue

        if path.name.casefold() == "put_update_file_here.txt":
            continue

        if path.suffix.casefold() not in ALLOWED_UPDATE_EXTENSIONS:
            continue

        try:
            created = creation_timestamp(path)
            modified = path.stat().st_mtime
            candidates.append((created, modified, path))
        except OSError:
            continue

    if not candidates:
        return None

    candidates.sort(key=lambda item: (item[0], item[1]), reverse=True)
    return candidates[0][2]


def resolve_plugin_path(folder_name: str, filename: str) -> Path | None:
    """Resolve one plugin file safely below plugins/<folder>/."""
    folder_parts = [part for part in folder_name.replace("\\", "/").split("/") if part]
    if len(folder_parts) != 1 or folder_parts[0] in {".", ".."}:
        return None
    folder = PLUGINS_DIR / folder_parts[0]
    candidate = (folder / filename.replace("\\", "/").lstrip("/")).resolve()
    try:
        candidate.relative_to(folder.resolve())
    except (ValueError, OSError):
        return None
    if not is_plugin_file(candidate):
        return None
    return candidate


def plugin_catalog() -> list[dict]:
    result = []
    if not PLUGINS_DIR.exists():
        return result
    for folder in sorted((p for p in PLUGINS_DIR.iterdir() if p.is_dir() and not p.name.startswith("_")), key=lambda p: p.name.casefold()):
        files = [p for p in folder.rglob("*") if is_plugin_file(p)]
        result.append({
            "folder": folder.name,
            "plugins": [
                {"name": p.name, "size": p.stat().st_size, "download": f"/plugin/{quote(folder.name)}/{quote(p.relative_to(folder).as_posix(), safe='/')}"}
                for p in sorted(files, key=lambda p: p.name.casefold())
            ],
        })
    return result


class UpdateHandler(SimpleHTTPRequestHandler):
    server_version = "VimpulseUpdateServer/2.2"

    def log_message(self, fmt: str, *args) -> None:
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        message = f"[{timestamp}] {self.client_address[0]} - {fmt % args}"
        print(message)
        try:
            with LOG_FILE.open("a", encoding="utf-8") as log:
                log.write(message + "\n")
        except OSError:
            pass

    def end_headers(self) -> None:
        self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
        self.send_header("Pragma", "no-cache")
        self.send_header("Expires", "0")
        self.send_header("X-Vimpulse-Update-Server", "5.0")
        super().end_headers()

    def _send_update_file(self, path: Path, send_body: bool):
        try:
            stat = path.stat()
            created = creation_timestamp(path)
            ctype = mimetypes.guess_type(str(path))[0] or "application/octet-stream"

            self.send_response(200)
            self.send_header("Content-Type", ctype)
            self.send_header("Content-Length", str(stat.st_size))
            self.send_header("Last-Modified", formatdate(stat.st_mtime, usegmt=True))

            self.send_header("X-File-Created", formatdate(created, usegmt=True))
            self.send_header("X-File-Created-Unix", str(int(created)))
            self.send_header("X-Update-Filename", path.name)
            self.send_header(
                "Content-Disposition",
                f'attachment; filename="{path.name.replace(chr(34), "")}"'
            )
            self.end_headers()

            if send_body:
                with path.open("rb") as f:
                    while True:
                        chunk = f.read(1024 * 1024)
                        if not chunk:
                            break
                        self.wfile.write(chunk)

        except OSError:
            self.send_error(404, "Update file not found")

    def _handle_latest(self, send_body: bool) -> bool:
        """
        Special endpoint:
            /latest/<APP_FOLDER_NAME>

        Example:
            /latest/StoryCut
            /latest/StoryRealm%20Studio
            /latest/rigcore

        It returns whichever valid update file in the app's updates folder
        has the newest creation timestamp. The client never needs the filename.
        """
        parsed = urlparse(self.path)
        route = unquote(parsed.path)

        prefix = "/latest/"
        if not route.casefold().startswith(prefix.casefold()):
            return False

        app_name = route[len(prefix):].strip("/")

        if not app_name:
            self.send_error(400, "Application name required")
            return True

        latest = find_latest_update(app_name)

        if latest is None:
            self.send_error(404, f"No update file found for {app_name}")
            return True

        self._send_update_file(latest, send_body=send_body)
        return True

    def do_HEAD(self):
        if self._handle_latest(send_body=False):
            return
        super().do_HEAD()

    def do_GET(self):
        parsed = urlparse(self.path)
        if parsed.path == "/health":
            payload = json.dumps({"success": True, "service": "Vimpulse Update API", "status": "online", "port": PORT}).encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Content-Length", str(len(payload)))
            self.end_headers()
            self.wfile.write(payload)
            return
        if parsed.path.startswith("/metadata/"):
            app_name = unquote(parsed.path[len("/metadata/"):]).strip("/")
            latest = find_latest_update(app_name)
            if latest is None:
                self.send_error(404, "Update metadata not found")
                return
            stat = latest.stat()
            payload = json.dumps({
                "product": app_name,
                "version": latest.stem,
                "filename": latest.name,
                "size": stat.st_size,
                "updated": datetime.fromtimestamp(stat.st_mtime).isoformat(timespec="seconds"),
                "download": f"/latest/{quote(app_name, safe='/')}",
            }).encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Content-Length", str(len(payload)))
            self.end_headers()
            self.wfile.write(payload)
            return
        if parsed.path == "/plugins":
            payload = json.dumps(plugin_catalog(), ensure_ascii=False).encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Content-Length", str(len(payload)))
            self.end_headers()
            self.wfile.write(payload)
            return
        if parsed.path.startswith("/plugin/"):
            parts = [unquote(part) for part in parsed.path.split("/")[2:] if part]
            if len(parts) >= 2:
                plugin = resolve_plugin_path(parts[0], "/".join(parts[1:]))
                if plugin:
                    self._send_update_file(plugin, send_body=True)
                    return
            self.send_error(404, "Plugin not found")
            return
        if self._handle_latest(send_body=True):
            return
        super().do_GET()

    def send_head(self):
        """
        Standard file serving with Vimpulse timestamp headers.
        """
        path = Path(self.translate_path(self.path))

        if path.is_dir():
            return super().send_head()

        if not path.exists() or not path.is_file():
            self.send_error(404, "File not found")
            return None

        try:
            file_obj = path.open("rb")
        except OSError:
            self.send_error(404, "File not found")
            return None

        try:
            stat = path.stat()
            created = creation_timestamp(path)
            ctype = mimetypes.guess_type(str(path))[0] or "application/octet-stream"

            self.send_response(200)
            self.send_header("Content-Type", ctype)
            self.send_header("Content-Length", str(stat.st_size))
            self.send_header("Last-Modified", formatdate(stat.st_mtime, usegmt=True))
            self.send_header("X-File-Created", formatdate(created, usegmt=True))
            self.send_header("X-File-Created-Unix", str(int(created)))
            self.send_header("X-Update-Filename", path.name)
            self.end_headers()
            return file_obj

        except Exception:
            file_obj.close()
            raise


def main() -> None:
    print()
    print("=" * 64)
    print("               VIMPULSE UPDATE SERVER v5")
    print("=" * 64)
    print()
    print(f"Listening on: {HOST}:{PORT}")
    print(f"Internal: http://10.0.0.30:{PORT}")
    print(f"Public:   http://99.232.137.208:{PORT}")
    print()
    print("LATEST UPDATE ENDPOINT:")
    print("  http://99.232.137.208:8765/latest/<APP>")
    print()
    print("Examples:")
    print("  http://99.232.137.208:8765/latest/StoryCut")
    print("  http://99.232.137.208:8765/latest/StoryRealm%20Studio")
    print("  http://99.232.137.208:8765/latest/rigcore")
    print("  http://99.232.137.208:8765/latest/vimpulsetechtoolkit/Pro")
    print("  http://99.232.137.208:8765/latest/vimpulsetechtoolkit/Business")
    print("  http://99.232.137.208:8765/plugins")
    print()
    print("The update filename can be ANY name.")
    print("Plugin folders accept any file extension.")
    print("The newest supported file in each app's updates folder is used.")
    print()
    print("Supported update files:")
    print("  .exe  .msi  .msix  .msixbundle  .zip")
    print()
    print("Press CTRL+C or close the run.bat window to stop the server.")
    print()

    server = ThreadingHTTPServer((HOST, PORT), UpdateHandler)

    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\nStopping Vimpulse Update Server...")
    finally:
        server.server_close()


if __name__ == "__main__":
    main()
