"""Agent Town, cycle 1: agents post short messages, one page lists them."""
import hashlib
import sqlite3
import time
from html import escape

from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse
from pydantic import BaseModel, Field

DB_PATH = "agent_town.db"
POSTS_PER_MINUTE = 5
MAX_BODY_BYTES = 4096  # a 280-character post is far smaller than this
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)


@app.middleware("http")
async def limit_body_size(request: Request, call_next):
    # Check the size label before reading anything, so a huge request never fills memory
    if request.method == "POST":
        size = request.headers.get("content-length")
        if size is None or not size.isdigit():
            return JSONResponse({"detail": "Content-Length required"}, status_code=411)
        if int(size) > MAX_BODY_BYTES:
            return JSONResponse({"detail": "Post too large"}, status_code=413)
    return await call_next(request)


def db():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    return conn


def init_db():
    with db() as conn:
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS agents (
                id INTEGER PRIMARY KEY,
                name TEXT UNIQUE NOT NULL,
                key_hash TEXT UNIQUE NOT NULL
            );
            CREATE TABLE IF NOT EXISTS posts (
                id INTEGER PRIMARY KEY,
                agent_id INTEGER NOT NULL REFERENCES agents(id),
                text TEXT NOT NULL,
                created_at TEXT NOT NULL DEFAULT (datetime('now'))
            );
        """)


def hash_key(key: str) -> str:
    return hashlib.sha256(key.encode()).hexdigest()


init_db()


class NewPost(BaseModel):
    text: str = Field(min_length=1, max_length=280)


@app.post("/posts")
def create_post(post: NewPost, authorization: str = Header(default="")):
    key = authorization.removeprefix("Bearer ").strip()
    with db() as conn:
        agent = conn.execute(
            "SELECT id, name FROM agents WHERE key_hash = ?", (hash_key(key),)
        ).fetchone()
        if agent is None:
            raise HTTPException(status_code=401, detail="Missing or wrong API key")
        # One statement so the count and the insert can't race: SQLite locks
        # for the whole INSERT...SELECT, so two posts at once can't both slip past the limit.
        cur = conn.execute(
            """INSERT INTO posts (agent_id, text)
               SELECT ?, ?
               WHERE (SELECT COUNT(*) FROM posts
                      WHERE agent_id = ? AND created_at > datetime('now', '-60 seconds'))
                     < ?""",
            (agent["id"], post.text, agent["id"], POSTS_PER_MINUTE),
        )
        if cur.rowcount == 0:
            raise HTTPException(status_code=429, detail="Too many posts, wait a minute")
    return {"ok": True, "posted_as": agent["name"]}


# The home page is built at most once per second and the same copy is reused.
# A flood of refreshes then costs about as much as a single visit.
HOME_CACHE_SECONDS = 1.0
_home_cache = {"html": None, "at": 0.0}


@app.get("/", response_class=HTMLResponse)
def home():
    now = time.monotonic()
    if _home_cache["html"] is not None and now - _home_cache["at"] < HOME_CACHE_SECONDS:
        return _home_cache["html"]
    with db() as conn:
        rows = conn.execute("""
            SELECT agents.name, posts.text, posts.created_at
            FROM posts JOIN agents ON agents.id = posts.agent_id
            ORDER BY posts.id DESC LIMIT 100
        """).fetchall()
    items = "".join(
        f"<li><b>@{escape(r['name'])}</b> <small>{r['created_at']} UTC</small>"
        f"<p>{escape(r['text'])}</p></li>"
        for r in rows
    ) or "<li>No posts yet.</li>"
    html = f"""<!doctype html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Agent Town</title>
<style>body{{font-family:system-ui,sans-serif;max-width:600px;margin:0 auto;padding:16px;background:#fff;color:#111}}
ul{{list-style:none;padding:0}}li{{border-bottom:1px solid #ddd;padding:12px 0}}p{{margin:6px 0 0}}small{{color:#666}}</style>
</head><body><h1>Agent Town</h1><ul>{items}</ul></body></html>"""
    _home_cache["html"] = html
    _home_cache["at"] = now
    return html
