import argparse
import html
import json
import os
import random
import re
import sys
from pathlib import Path
from string import Template

import aiosqlite
import uvicorn
from fastapi import FastAPI, Request
from fastapi.responses import (
    HTMLResponse,
    PlainTextResponse,
    Response,
)


def default_db_path() -> str:
    if sys.platform == "win32":
        base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
    elif sys.platform == "darwin":
        base = Path.home() / "Library" / "Application Support"
    else:
        base = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
    data_dir = base / "clipbox"
    data_dir.mkdir(parents=True, exist_ok=True)
    return str(data_dir / "clipbox.db")


app = FastAPI()
DB_PATH = os.environ.get("CLIPBOX_DB", default_db_path())

WORDS = [
    "alpha",
    "bravo",
    "charlie",
    "delta",
    "echo",
    "foxtrot",
    "golf",
    "hotel",
    "india",
    "juliet",
    "kilo",
    "lima",
    "mike",
    "november",
    "oscar",
    "papa",
    "quebec",
    "romeo",
    "sierra",
    "tango",
    "uniform",
    "victor",
    "whiskey",
    "xray",
    "yankee",
    "zulu",
]


async def generate_name(db: aiosqlite.Connection) -> str:
    rows = await db.execute_fetchall("SELECT name FROM buffers")
    taken = {r[0] for r in rows}
    available = [w for w in WORDS if w not in taken]
    if available:
        return random.choice(available)
    # Fallback: word + number
    for _ in range(100):
        name = random.choice(WORDS) + str(random.randint(1, 999))
        if name not in taken:
            return name
    return "buffer-" + str(random.randint(1000, 9999))


ICON_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
  <rect x="0" y="0" width="128" height="128" rx="28" fill="#1f1f1f"/>
  <rect x="44" y="12" width="40" height="18" rx="6" fill="#828282"/>
  <rect x="36" y="48" width="56" height="5" rx="2" fill="#666"/>
  <rect x="36" y="64" width="46" height="5" rx="2" fill="#666"/>
  <rect x="36" y="80" width="50" height="5" rx="2" fill="#666"/>
  <rect x="36" y="96" width="38" height="5" rx="2" fill="#666"/>
</svg>"""

MANIFEST_JSON = json.dumps(
    {
        "name": "clipbox",
        "short_name": "clipbox",
        "start_url": "/",
        "display": "standalone",
        "background_color": "#0d0d0d",
        "theme_color": "#0d0d0d",
        "icons": [{"src": "/icon.svg", "sizes": "any", "type": "image/svg+xml"}],
    }
)


@app.get("/icon.svg")
async def icon():
    return Response(content=ICON_SVG, media_type="image/svg+xml")


@app.get("/manifest.json")
async def manifest():
    return Response(content=MANIFEST_JSON, media_type="application/manifest+json")


async def get_db():
    db = await aiosqlite.connect(DB_PATH)
    await db.execute("PRAGMA journal_mode=WAL")
    await db.execute(
        "CREATE TABLE IF NOT EXISTS buffers ("
        "  name TEXT PRIMARY KEY,"
        "  content TEXT NOT NULL DEFAULT '',"
        "  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
        ")"
    )
    await db.commit()
    return db


def wants_html(request: Request) -> bool:
    return "text/html" in request.headers.get("accept", "")


# ---------------------------------------------------------------------------
# Index
# ---------------------------------------------------------------------------

INDEX_TEMPLATE = Template("""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>clipbox</title>
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/icon.svg">
<link rel="manifest" href="/manifest.json">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#0d0d0d">
<style>
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  :root {
    --bg: #0d0d0d; --surface: #1f1f1f; --border: #333;
    --text: #e0e0e0; --muted: #888; --accent: #aaa; --green: #ccc;
  }
  body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background: var(--bg); color: var(--text);
    min-height: 100dvh; padding: 2rem 1rem;
  }
  .container { max-width: 600px; margin: 0 auto; }
  h1 { font-size: 1.5rem; margin-bottom: 1.5rem; display: flex; align-items: center; gap: 0.5rem; }
  h1 img { width: 1.4em; height: 1.4em; }
  .new-form { display: flex; gap: 0.5rem; margin-bottom: 2rem; }
  .new-form textarea {
    flex: 1; background: var(--surface); border: 1px solid var(--border);
    color: var(--text); padding: 0.6rem 0.8rem; border-radius: 6px;
    font-size: 0.95rem; outline: none; resize: vertical; min-height: 3rem;
    font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
  }
  .new-form textarea:focus { border-color: var(--accent); }
  button {
    background: #828282; color: #fff; border: none;
    padding: 0.6rem 1.2rem; border-radius: 6px; cursor: pointer; font-size: 0.9rem;
  }
  button:hover { opacity: 0.85; }
  .buffers { list-style: none; }
  .buffers li { border-bottom: 1px solid var(--border); }
  .buffers li {
    display: flex; align-items: center;
  }
  .buffers a {
    flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.2rem;
    padding: 0.75rem 0.5rem; color: var(--text); text-decoration: none;
    border-radius: 4px; transition: background 0.1s;
  }
  .buffers a:hover { background: var(--surface); }
  .buffers .top-row { display: flex; justify-content: space-between; align-items: center; width: 100%; }
  .buffers .name { font-weight: 500; }
  .buffers .meta { font-size: 0.8rem; color: var(--muted); }
  .buffers .preview {
    font-size: 0.8rem; color: var(--muted);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
  }
  .buffers .copy-btn {
    background: none; color: var(--muted); border: none; cursor: pointer;
    padding: 0.5rem 1.5rem; font-size: 1.5rem; border-radius: 4px;
    transition: color 0.15s, background 0.15s; flex-shrink: 0;
  }
  .buffers .copy-btn:hover { background: var(--surface); }
  .buffers .copy-btn.copy-ok { color: var(--green); }
  .buffers .del-btn {
    background: none; color: var(--muted); border: none; cursor: pointer;
    padding: 0.4rem 0.5rem; font-size: 0.75rem; border-radius: 4px;
    transition: color 0.15s; flex-shrink: 0;
  }
  .buffers .del-btn:hover { color: #fff; }
  .empty { color: var(--muted); font-style: italic; }
  .hint {
    margin-top: 2rem; padding: 1rem; background: var(--surface);
    border-radius: 8px; font-size: 0.85rem; color: var(--muted); line-height: 1.6;
  }
  .hint code {
    background: var(--border); padding: 0.15rem 0.4rem; border-radius: 3px;
    color: var(--green);
  }
  .usage { margin-top: 0.5rem; border-spacing: 0; }
  .usage td { padding: 0.15rem 0; }
  .usage td:first-child { padding-right: 0.75rem; white-space: nowrap; }
</style>
</head>
<body>
<div class="container">
  <h1><img src="/icon.svg" alt="">clipbox</h1>
  <div class="new-form">
    <textarea id="new-content" placeholder="Paste or type text..."></textarea>
    <button onclick="createBuf()">Create</button>
  </div>
  $buffer_list
  <div class="hint">
    <strong>curl usage:</strong>
    <table class="usage"><tbody>
      <tr><td>Read</td><td><code>curl http://host:port/name</code></td></tr>
      <tr><td>Write</td><td><code>curl -X PUT -d 'text' http://host:port/name</code></td></tr>
      <tr><td>Rename</td><td><code>curl -X PATCH -d 'new' http://host:port/name</code></td></tr>
      <tr><td>Delete</td><td><code>curl -X DELETE http://host:port/name</code></td></tr>
    </tbody></table>
    <table class="usage"><tbody>
      <tr><td>List all buffers</td><td><code>curl http://host:port/api/buffers</code></td></tr>
    </tbody></table>
  </div>
</div>
<script>
async function createBuf() {
  var content = document.getElementById("new-content").value;
  var res = await fetch("/api/create", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ content: content })
  });
  if (res.ok) {
    var data = await res.json();
    window.location.href = "/" + encodeURIComponent(data.name);
  }
}
async function copyBuf(btn) {
  var text = btn.getAttribute("data-content");
  try {
    await navigator.clipboard.writeText(text);
  } catch (e) {
    var ta = document.createElement("textarea");
    ta.value = text;
    ta.style.position = "fixed";
    ta.style.opacity = "0";
    document.body.appendChild(ta);
    ta.focus();
    ta.select();
    document.execCommand("copy");
    document.body.removeChild(ta);
  }
  var orig = btn.innerHTML;
  btn.innerHTML = "&#x2713;";
  btn.classList.add("copy-ok");
  setTimeout(function() { btn.innerHTML = orig; btn.classList.remove("copy-ok"); }, 1500);
}
async function deleteBuf(name) {
  if (!confirm("Delete buffer '" + name + "'?")) return;
  const res = await fetch("/" + encodeURIComponent(name), { method: "DELETE" });
  if (res.ok) location.reload();
}
</script>
</body>
</html>""")


HELP_TEXT = """clipbox - shared clipboard

usage:
  curl http://host:port/<name>                          read a buffer
  curl -X PUT -d 'text' http://host:port/<name>         write to a buffer
  curl -X PATCH -d 'new-name' http://host:port/<name>   rename a buffer
  curl -X DELETE http://host:port/<name>                delete a buffer
  curl http://host:port/api/buffers                     list all buffers (JSON)
"""


@app.get("/")
async def index(request: Request):
    if not wants_html(request):
        return PlainTextResponse(HELP_TEXT)
    db = await get_db()
    try:
        rows = await db.execute_fetchall(
            "SELECT name, content, length(content), updated_at FROM buffers ORDER BY updated_at DESC"
        )
        if rows:
            items = []
            for name, content, size, _updated in rows:
                size_str = f"{size} chars" if size else "empty"
                esc = html.escape(name)
                content_attr = html.escape(content or "", quote=True)
                preview = (
                    html.escape((content or "").split("\n", 1)[0][:120]) or "empty"
                )
                items.append(
                    f'<li data-name="{esc}">'
                    f'<button class="del-btn" onclick="deleteBuf(\'{esc}\')"'
                    f' title="Delete buffer">&#x2715;</button>'
                    f'<a href="/{esc}"><span class="top-row"><span class="name">{esc}</span>'
                    f'<span class="meta">{size_str}</span></span>'
                    f'<span class="preview">{preview}</span></a>'
                    f'<button class="copy-btn" data-content="{content_attr}" onclick="copyBuf(this)"'
                    f' title="Copy contents">&#x2398;</button></li>'
                )
            buffer_list = '<ul class="buffers">' + "".join(items) + "</ul>"
        else:
            buffer_list = '<p class="empty">No buffers yet. Create one above.</p>'
    finally:
        await db.close()
    return HTMLResponse(INDEX_TEMPLATE.substitute(buffer_list=buffer_list))


@app.get("/api/buffers")
async def api_list_buffers():
    db = await get_db()
    try:
        rows = await db.execute_fetchall(
            "SELECT name, content, length(content), updated_at FROM buffers ORDER BY updated_at DESC"
        )
    finally:
        await db.close()
    buffers = [
        {"name": name, "content": content, "size": size, "updated_at": updated}
        for name, content, size, updated in rows
    ]
    return Response(content=json.dumps(buffers), media_type="application/json")


@app.post("/api/create")
async def api_create_buffer(request: Request):
    body = await request.json()
    content = body.get("content", "")
    db = await get_db()
    try:
        name = await generate_name(db)
        await db.execute(
            "INSERT INTO buffers (name, content, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)",
            (name, content),
        )
        await db.commit()
    finally:
        await db.close()
    return Response(content=json.dumps({"name": name}), media_type="application/json")


@app.post("/api/rename")
async def api_rename_buffer(request: Request):
    body = await request.json()
    old_name = body.get("old_name", "")
    new_name = re.sub(r"[^a-zA-Z0-9_\-]", "", body.get("new_name", "").strip())
    if not old_name or not new_name or old_name == new_name:
        return Response(
            content=json.dumps({"error": "invalid"}),
            media_type="application/json",
            status_code=400,
        )
    db = await get_db()
    try:
        # Check new name isn't taken
        existing = await db.execute_fetchall(
            "SELECT 1 FROM buffers WHERE name = ?", (new_name,)
        )
        if existing:
            return Response(
                content=json.dumps({"error": "name taken"}),
                media_type="application/json",
                status_code=409,
            )
        await db.execute(
            "UPDATE buffers SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?",
            (new_name, old_name),
        )
        await db.commit()
    finally:
        await db.close()
    return Response(
        content=json.dumps({"name": new_name}), media_type="application/json"
    )


# ---------------------------------------------------------------------------
# Buffer editor
# ---------------------------------------------------------------------------

PAGE_TEMPLATE = Template("""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>$name_esc — clipbox</title>
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/icon.svg">
<link rel="manifest" href="/manifest.json">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#0d0d0d">
<style>
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  :root {
    --bg: #0d0d0d; --surface: #1f1f1f; --border: #333;
    --text: #e0e0e0; --muted: #888; --accent: #aaa;
    --accent-hover: #ccc; --green: #ccc;
  }
  body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background: var(--bg); color: var(--text);
    min-height: 100dvh; display: flex; flex-direction: column;
  }
  header {
    display: flex; align-items: center; justify-content: space-between;
    padding: 0.75rem 1rem; border-bottom: 1px solid var(--border);
    flex-wrap: wrap; gap: 0.5rem;
  }
  header .left { display: flex; align-items: center; gap: 0.6rem; }
  header h1 { font-size: 1.1rem; font-weight: 600; }
  header h1 a { color: var(--muted); text-decoration: none; }
  header h1 .buf { color: var(--text); cursor: pointer; }
  header h1 .buf:hover { text-decoration: underline; text-decoration-style: dotted; }
  header h1 .buf-input {
    background: var(--surface); border: 1px solid var(--border); color: var(--text);
    font-size: inherit; font-weight: inherit; padding: 0.1rem 0.3rem; border-radius: 4px;
    outline: none;
  }
  header h1 .buf-input:focus { border-color: var(--accent); }
  .status {
    font-size: 0.8rem; color: var(--muted); transition: color 0.2s;
  }
  .status.saved { color: var(--green); }
  .status.saving { color: var(--accent); }
  .status.error { color: #999; }
  .actions { display: flex; gap: 0.5rem; align-items: center; }
  button {
    background: #828282; color: #fff; border: none;
    padding: 0.45rem 0.9rem; border-radius: 6px; cursor: pointer;
    font-size: 0.85rem; transition: background 0.15s;
  }
  button:hover { opacity: 0.85; }
  button:active { transform: scale(0.97); }
  .copy-ok { background: #444 !important; color: #fff !important; }
  #del-btn { background: transparent; color: var(--muted); }
  #del-btn:hover { background: #444; color: #fff; }
  .editor-wrap { flex: 1; display: flex; padding: 0.5rem; }
  textarea {
    flex: 1; background: var(--surface); color: var(--text);
    border: 1px solid var(--border); border-radius: 8px; padding: 1rem;
    font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
    font-size: 0.95rem; line-height: 1.5; resize: none; outline: none;
  }
  textarea:focus { border-color: var(--accent); }
  textarea::placeholder { color: var(--muted); }
</style>
</head>
<body>

<header>
  <div class="left">
    <h1><a href="/">clipbox</a> / <span class="buf" id="buf-name" onclick="startRename()">$name_esc</span></h1>
    <span class="status" id="status"></span>
  </div>
  <div class="actions">
    <button id="copy-btn" title="Copy to clipboard">Copy</button>
    <button id="del-btn" title="Delete buffer">Delete</button>
  </div>
</header>

<div class="editor-wrap">
  <textarea id="editor" placeholder="Type or paste here..." spellcheck="false">$content_esc</textarea>
</div>

<script>
(function() {
  const editor = document.getElementById("editor");
  const status = document.getElementById("status");
  const copyBtn = document.getElementById("copy-btn");
  const bufName = $name_json;

  let saveTimer = null;
  let lastSaved = editor.value;

  function setStatus(text, cls) {
    status.textContent = text;
    status.className = "status " + (cls || "");
  }

  async function save() {
    const content = editor.value;
    if (content === lastSaved) return;
    setStatus("Saving...", "saving");
    try {
      const res = await fetch("/" + encodeURIComponent(bufName), {
        method: "PUT",
        headers: { "Content-Type": "text/plain" },
        body: content
      });
      if (res.ok) {
        lastSaved = content;
        setStatus("Saved", "saved");
      } else {
        setStatus("Error saving", "error");
      }
    } catch (e) {
      setStatus("Connection lost", "error");
    }
  }

  editor.addEventListener("input", function() {
    clearTimeout(saveTimer);
    setStatus("Editing...", "");
    saveTimer = setTimeout(save, 400);
  });

  editor.addEventListener("blur", save);
  document.addEventListener("visibilitychange", function() {
    if (document.visibilityState === "hidden") save();
  });

  copyBtn.addEventListener("click", async function() {
    try {
      await navigator.clipboard.writeText(editor.value);
    } catch (e) {
      var ta = document.createElement("textarea");
      ta.value = editor.value;
      ta.style.position = "fixed";
      ta.style.opacity = "0";
      document.body.appendChild(ta);
      ta.focus();
      ta.select();
      document.execCommand("copy");
      document.body.removeChild(ta);
    }
    copyBtn.textContent = "Copied!";
    copyBtn.classList.add("copy-ok");
    setTimeout(() => {
      copyBtn.textContent = "Copy";
      copyBtn.classList.remove("copy-ok");
    }, 1500);
  });

  document.getElementById("del-btn").addEventListener("click", async function() {
    if (!confirm("Delete buffer '" + bufName + "'?")) return;
    const res = await fetch("/" + encodeURIComponent(bufName), { method: "DELETE" });
    if (res.ok) window.location.href = "/";
  });

  window.startRename = function() {
    var span = document.getElementById("buf-name");
    var input = document.createElement("input");
    input.type = "text";
    input.className = "buf-input";
    input.value = bufName;
    input.size = Math.max(bufName.length, 6);
    span.replaceWith(input);
    input.focus();
    input.select();
    async function doRename() {
      var newName = input.value.replace(/[^a-zA-Z0-9_\\-]/g, "").trim();
      if (!newName || newName === bufName) {
        location.reload();
        return;
      }
      var res = await fetch("/api/rename", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ old_name: bufName, new_name: newName })
      });
      if (res.ok) {
        window.location.href = "/" + encodeURIComponent(newName);
      } else {
        alert("Rename failed");
        location.reload();
      }
    }
    input.addEventListener("blur", doRename);
    input.addEventListener("keydown", function(e) {
      if (e.key === "Enter") { e.preventDefault(); input.blur(); }
      if (e.key === "Escape") { location.reload(); }
    });
  };

  if (editor.value.length > 0) setStatus("Saved", "saved");
})();
</script>
</body>
</html>""")


@app.get("/{name}")
async def get_buffer(name: str, request: Request):
    db = await get_db()
    try:
        row = await db.execute_fetchall(
            "SELECT content FROM buffers WHERE name = ?", (name,)
        )
    finally:
        await db.close()
    content = row[0][0] if row else ""

    if not wants_html(request):
        return PlainTextResponse(content)

    return HTMLResponse(
        PAGE_TEMPLATE.substitute(
            name_esc=html.escape(name),
            content_esc=html.escape(content),
            name_json=json.dumps(name),
        )
    )


@app.put("/")
async def put_buffer_auto(request: Request):
    content = (await request.body()).decode()
    db = await get_db()
    try:
        name = await generate_name(db)
        await db.execute(
            "INSERT INTO buffers (name, content, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)",
            (name, content),
        )
        await db.commit()
    finally:
        await db.close()
    return PlainTextResponse(f"created: {name}\n")


@app.put("/{name}")
async def put_buffer(name: str, request: Request):
    content = (await request.body()).decode()
    db = await get_db()
    try:
        await db.execute(
            "INSERT INTO buffers (name, content, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) "
            "ON CONFLICT(name) DO UPDATE SET content=excluded.content, updated_at=excluded.updated_at",
            (name, content),
        )
        await db.commit()
    finally:
        await db.close()
    return PlainTextResponse("ok")


@app.patch("/{name}")
async def patch_buffer(name: str, request: Request):
    new_name = (await request.body()).decode().strip()
    new_name = re.sub(r"[^a-zA-Z0-9_\-]", "", new_name)
    if not new_name or new_name == name:
        return PlainTextResponse("invalid name\n", status_code=400)
    db = await get_db()
    try:
        existing = await db.execute_fetchall(
            "SELECT 1 FROM buffers WHERE name = ?", (new_name,)
        )
        if existing:
            return PlainTextResponse("name taken\n", status_code=409)
        await db.execute(
            "UPDATE buffers SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?",
            (new_name, name),
        )
        await db.commit()
    finally:
        await db.close()
    return PlainTextResponse(f"renamed: {name} -> {new_name}\n")


@app.delete("/{name}")
async def delete_buffer(name: str):
    db = await get_db()
    try:
        await db.execute("DELETE FROM buffers WHERE name = ?", (name,))
        await db.commit()
    finally:
        await db.close()
    return PlainTextResponse("ok")


def main():
    parser = argparse.ArgumentParser(description="clipbox - shared clipboard")
    parser.add_argument("-p", "--port", type=int, default=8080)
    parser.add_argument("--host", default="0.0.0.0")
    args = parser.parse_args()
    print(f"clipbox running on http://{args.host}:{args.port}")
    uvicorn.run(app, host=args.host, port=args.port, log_level="warning")


if __name__ == "__main__":
    main()
