#!/usr/bin/env python3
"""Tiny local dashboard for Frezer's Etsy + Printify product factory.

No external dependencies. Serves:
- /              browser dashboard
- /api/status    JSON summary
- /api/files     JSON file list
- /files/<path>  generated files under the engine root
"""
from __future__ import annotations

import argparse
import html
import json
import mimetypes
import os
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import unquote, urlparse

ENGINE_ROOT = Path(os.environ.get("ETSY_ENGINE_ROOT", "/root/etsy_printify_revenue_engine"))
ENV_PATH = Path(os.environ.get("HERMES_ENV_PATH", "/root/.hermes/.env"))

IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg"}
DATA_EXTS = {".csv", ".json", ".jsonl", ".xlsx"}
DOC_EXTS = {".md", ".txt", ".pdf"}
DESIGN_EXTS = {".psd", ".ai", ".eps", ".svg"}


def read_env_value(env_path: Path, key: str) -> str | None:
    if not env_path.exists():
        return None
    for raw in env_path.read_text(encoding="utf-8", errors="ignore").splitlines():
        line = raw.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        k, v = line.split("=", 1)
        if k.strip() == key:
            return v.strip().strip('"').strip("'")
    return None


def classify_file(path: Path) -> str:
    ext = path.suffix.lower()
    if ext in IMAGE_EXTS:
        return "image/design"
    if ext in DATA_EXTS:
        return "listing/data"
    if ext in DOC_EXTS:
        return "notes/docs"
    if ext in DESIGN_EXTS:
        return "design/source"
    return "other"


def list_generated_files(root: Path = ENGINE_ROOT, limit: int = 250) -> list[dict]:
    root = root.resolve()
    if not root.exists():
        return []
    rows: list[dict] = []
    for path in root.rglob("*"):
        if not path.is_file():
            continue
        rel = path.relative_to(root)
        if any(part.startswith(".") or part == "__pycache__" for part in rel.parts):
            continue
        try:
            stat = path.stat()
        except OSError:
            continue
        rows.append(
            {
                "name": path.name,
                "relative_path": rel.as_posix(),
                "type": classify_file(path),
                "size_bytes": stat.st_size,
                "modified_at": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
                "url": "/files/" + rel.as_posix(),
            }
        )
    rows.sort(key=lambda item: item["modified_at"], reverse=True)
    return rows[:limit]


def build_status(root: Path = ENGINE_ROOT, env_path: Path = ENV_PATH) -> dict:
    files = list_generated_files(root)
    image_count = sum(1 for f in files if f["type"] == "image/design")
    daily_dir = root / "daily_batches"
    daily_batches = 0
    if daily_dir.exists():
        daily_batches = sum(1 for p in daily_dir.iterdir() if p.is_file() and not p.name.startswith("."))
    return {
        "updated_at": datetime.now(timezone.utc).isoformat(),
        "engine_root": str(root),
        "shop_id": read_env_value(env_path, "PRINTIFY_SHOP_ID"),
        "printify_token_present": bool(read_env_value(env_path, "PRINTIFY_API_TOKEN")),
        "openai_key_present": bool(read_env_value(env_path, "OPENAI_API_KEY")),
        "files_total": len(files),
        "images_total": image_count,
        "daily_batches": daily_batches,
        "recent_files": files[:12],
    }


def format_size(n: int) -> str:
    units = ["B", "KB", "MB", "GB"]
    size = float(n)
    for unit in units:
        if size < 1024 or unit == units[-1]:
            return f"{size:.1f} {unit}" if unit != "B" else f"{int(size)} B"
        size /= 1024
    return f"{n} B"


def render_dashboard(root: Path = ENGINE_ROOT, env_path: Path = ENV_PATH) -> str:
    status = build_status(root, env_path)
    files = list_generated_files(root)
    cards = [
        ("Printify shop", status["shop_id"] or "not set"),
        ("Printify token", "connected" if status["printify_token_present"] else "missing"),
        ("OpenAI images", "ready" if status["openai_key_present"] else "missing key"),
        ("Generated files", str(status["files_total"])),
        ("Images/designs", str(status["images_total"])),
        ("Daily batches", str(status["daily_batches"])),
    ]
    card_html = "".join(
        f'<div class="card"><div class="label">{html.escape(label)}</div><div class="value">{html.escape(value)}</div></div>'
        for label, value in cards
    )
    rows = []
    for f in files[:100]:
        thumb = ""
        if f["type"] == "image/design":
            thumb = f'<a href="{html.escape(f["url"])}" target="_blank"><img src="{html.escape(f["url"])}" loading="lazy" /></a>'
        rows.append(
            "<tr>"
            f"<td>{thumb}</td>"
            f"<td><a href='{html.escape(f['url'])}' target='_blank'>{html.escape(f['relative_path'])}</a></td>"
            f"<td>{html.escape(f['type'])}</td>"
            f"<td>{format_size(f['size_bytes'])}</td>"
            f"<td>{html.escape(f['modified_at'].replace('T', ' ')[:19])} UTC</td>"
            "</tr>"
        )
    rows_html = "".join(rows) or '<tr><td colspan="5">No files yet.</td></tr>'
    return f"""<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta http-equiv="refresh" content="20" />
  <title>GracedPrintCo Product Factory</title>
  <style>
    :root {{ color-scheme: dark; --bg:#070707; --panel:#111; --muted:#a7a7a7; --gold:#d6ad60; --line:#282828; }}
    body {{ margin:0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif; background:linear-gradient(135deg,#060606,#13100a); color:#f4f1e8; }}
    header {{ padding:28px 32px 16px; border-bottom:1px solid var(--line); }}
    h1 {{ margin:0; font-size:28px; letter-spacing:-0.03em; }}
    .sub {{ color:var(--muted); margin-top:6px; }}
    .wrap {{ padding:24px 32px 40px; }}
    .cards {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:14px; margin-bottom:24px; }}
    .card {{ background:rgba(17,17,17,.9); border:1px solid var(--line); border-radius:16px; padding:16px; box-shadow:0 10px 30px rgba(0,0,0,.25); }}
    .label {{ color:var(--muted); font-size:12px; text-transform:uppercase; letter-spacing:.08em; }}
    .value {{ font-size:22px; font-weight:750; margin-top:8px; color:var(--gold); }}
    .panel {{ background:rgba(17,17,17,.86); border:1px solid var(--line); border-radius:18px; overflow:hidden; }}
    .panel h2 {{ margin:0; padding:18px 20px; border-bottom:1px solid var(--line); font-size:18px; }}
    table {{ width:100%; border-collapse:collapse; }}
    th,td {{ padding:12px 14px; border-bottom:1px solid #202020; text-align:left; vertical-align:middle; }}
    th {{ color:var(--muted); font-size:12px; text-transform:uppercase; letter-spacing:.08em; }}
    a {{ color:#f1d492; text-decoration:none; }}
    a:hover {{ text-decoration:underline; }}
    img {{ width:74px; height:74px; object-fit:cover; border-radius:10px; border:1px solid #333; background:#222; }}
    .hint {{ margin-top:14px; color:var(--muted); font-size:13px; }}
    code {{ background:#1b1b1b; padding:2px 6px; border-radius:6px; color:#f1d492; }}
  </style>
</head>
<body>
  <header>
    <h1>GracedPrintCo Product Factory</h1>
    <div class="sub">Live view of Etsy/Printify files, images, listing batches, and automation artifacts. Auto-refreshes every 20 seconds.</div>
  </header>
  <main class="wrap">
    <section class="cards">{card_html}</section>
    <section class="panel">
      <h2>Generated files and images</h2>
      <table>
        <thead><tr><th>Preview</th><th>File</th><th>Type</th><th>Size</th><th>Modified</th></tr></thead>
        <tbody>{rows_html}</tbody>
      </table>
    </section>
    <div class="hint">API endpoints: <code>/api/status</code> and <code>/api/files</code>. Root: <code>{html.escape(str(root))}</code></div>
  </main>
</body>
</html>"""


class DashboardHandler(BaseHTTPRequestHandler):
    root = ENGINE_ROOT
    env_path = ENV_PATH

    def log_message(self, fmt: str, *args) -> None:  # quieter logs
        print(f"[{datetime.now().isoformat(timespec='seconds')}] {self.address_string()} {fmt % args}")

    def send_json(self, payload: object, status: int = 200) -> None:
        data = json.dumps(payload, indent=2).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    def do_GET(self) -> None:
        parsed = urlparse(self.path)
        path = parsed.path
        if path == "/":
            data = render_dashboard(self.root, self.env_path).encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "text/html; charset=utf-8")
            self.send_header("Content-Length", str(len(data)))
            self.end_headers()
            self.wfile.write(data)
            return
        if path == "/api/status":
            self.send_json(build_status(self.root, self.env_path))
            return
        if path == "/api/files":
            self.send_json(list_generated_files(self.root))
            return
        if path.startswith("/files/"):
            rel = unquote(path.removeprefix("/files/"))
            target = (self.root / rel).resolve()
            root_resolved = self.root.resolve()
            if not str(target).startswith(str(root_resolved)) or not target.is_file():
                self.send_error(404)
                return
            ctype = mimetypes.guess_type(target.name)[0] or "application/octet-stream"
            data = target.read_bytes()
            self.send_response(200)
            self.send_header("Content-Type", ctype)
            self.send_header("Content-Length", str(len(data)))
            self.end_headers()
            self.wfile.write(data)
            return
        self.send_error(404)


def main() -> None:
    parser = argparse.ArgumentParser(description="Run Etsy/Printify local dashboard")
    parser.add_argument("--host", default="127.0.0.1")
    parser.add_argument("--port", type=int, default=8787)
    parser.add_argument("--root", default=str(ENGINE_ROOT))
    parser.add_argument("--env", default=str(ENV_PATH))
    args = parser.parse_args()

    DashboardHandler.root = Path(args.root)
    DashboardHandler.env_path = Path(args.env)
    server = ThreadingHTTPServer((args.host, args.port), DashboardHandler)
    print(f"Dashboard running at http://{args.host}:{args.port}")
    print(f"Watching {DashboardHandler.root}")
    server.serve_forever()


if __name__ == "__main__":
    main()
