"""Local-only, intentionally incomplete Save draft exercise.

This is a training fixture, not an authentication or production example.
Only Python's standard library is used. See README.md before changing it.
"""

from __future__ import annotations

import argparse
import html
import json
import sqlite3
from http import HTTPStatus
from http.cookies import SimpleCookie
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlparse


IDENTITIES = {
    "author": "Avery Example — draft author",
    "reviewer": "Blair Example — second user",
}
DRAFT_ID = "fictional-draft-001"
SEED_TEXT = "The imaginary library opens next Tuesday."


class SimulatedSaveFailure(Exception):
    """Deterministic local stand-in for a failed write."""


class DraftStore:
    def __init__(self, path: Path):
        self.path = path
        self._initialize()

    def _connect(self):
        return sqlite3.connect(self.path)

    def _initialize(self):
        with self._connect() as connection:
            connection.execute(
                "CREATE TABLE IF NOT EXISTS drafts ("
                "id TEXT PRIMARY KEY, owner TEXT NOT NULL, content TEXT NOT NULL)"
            )
            connection.execute(
                "INSERT OR IGNORE INTO drafts (id, owner, content) VALUES (?, ?, ?)",
                (DRAFT_ID, "author", SEED_TEXT),
            )

    def get_by_id(self, draft_id: str):
        with self._connect() as connection:
            row = connection.execute(
                "SELECT id, owner, content FROM drafts WHERE id = ?", (draft_id,)
            ).fetchone()
        return dict(zip(("id", "owner", "content"), row)) if row else None

    def get_for_owner(self, owner: str):
        with self._connect() as connection:
            row = connection.execute(
                "SELECT id, owner, content FROM drafts WHERE owner = ? LIMIT 1",
                (owner,),
            ).fetchone()
        return dict(zip(("id", "owner", "content"), row)) if row else None

    def save(self, owner: str, content: str, simulate_failure: bool):
        if simulate_failure:
            raise SimulatedSaveFailure("The exercise write failed as requested")
        # EXERCISE DEFECT: a successful-looking call does not update SQLite.
        return DRAFT_ID


def page(identity: str | None, content: str = "", notice: str = "") -> bytes:
    if identity is None:
        main = """
          <h1>Save draft exercise</h1>
          <p>Choose one fictional test identity. This switcher is not real authentication.</p>
          <p><a href="/as/author">Avery Example — author</a></p>
          <p><a href="/as/reviewer">Blair Example — second user</a></p>
        """
    else:
        identity_label = html.escape(IDENTITIES[identity])
        safe_content = html.escape(content)
        notice_html = f'<p role="status">{html.escape(notice)}</p>' if notice else ""
        main = f"""
          <p class="top"><strong>{identity_label}</strong> · <a href="/logout">Switch identity</a></p>
          <h1>Project update</h1>
          <p>Write an update and save it for later. The current exercise has deliberate defects.</p>
          {notice_html}
          <form action="/save" method="post">
            <label for="draft-text">Draft text</label>
            <textarea id="draft-text" name="text" rows="9" required>{safe_content}</textarea>
            <div class="actions">
              <button type="submit">Save draft</button>
              <button type="submit" name="simulate_failure" value="1">Simulate failed save</button>
            </div>
          </form>
          <p class="hint">Reload the page or switch identities to test recovery and privacy.</p>
        """
    document = f"""<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Save draft — fictional exercise</title>
  <style>
    body {{ font: 1rem/1.5 system-ui, sans-serif; max-width: 46rem; margin: 2rem auto; padding: 0 1rem; color: #17212b; }}
    a {{ color: #065b8f; }}
    textarea {{ display: block; box-sizing: border-box; width: 100%; margin: .5rem 0 1rem; padding: .75rem; font: inherit; }}
    button {{ font: inherit; padding: .5rem .8rem; margin: 0 .5rem .5rem 0; }}
    .top, .hint {{ color: #465360; }}
  </style>
</head>
<body><main>{main}</main></body>
</html>"""
    return document.encode("utf-8")


def create_server(port: int = 8766, db_path: Path | None = None) -> ThreadingHTTPServer:
    """Always bind to loopback. Tests may choose port 0 for an ephemeral port."""
    store = DraftStore(db_path or Path(__file__).with_name("sandbox.sqlite3"))

    class Handler(BaseHTTPRequestHandler):
        def log_message(self, format_string, *args):
            # Keep the test run quiet; this server only accepts local exercise traffic.
            pass

        def _identity(self):
            cookie = SimpleCookie()
            try:
                cookie.load(self.headers.get("Cookie", ""))
            except Exception:
                return None
            item = cookie.get("demo_identity")
            identity = item.value if item else None
            return identity if identity in IDENTITIES else None

        def _respond(self, status, body=b"", content_type="text/html; charset=utf-8", cookie=None):
            self.send_response(status)
            self.send_header("Content-Type", content_type)
            self.send_header("Content-Length", str(len(body)))
            self.send_header("Cache-Control", "no-store")
            self.send_header("X-Content-Type-Options", "nosniff")
            if cookie:
                self.send_header("Set-Cookie", cookie)
            self.end_headers()
            self.wfile.write(body)

        def _redirect(self, cookie=None):
            self.send_response(HTTPStatus.SEE_OTHER)
            self.send_header("Location", "/")
            self.send_header("Content-Length", "0")
            self.send_header("Cache-Control", "no-store")
            if cookie:
                self.send_header("Set-Cookie", cookie)
            self.end_headers()

        def do_GET(self):
            path = urlparse(self.path).path
            if path in ("/as/author", "/as/reviewer"):
                identity = path.rsplit("/", 1)[1]
                self._redirect(f"demo_identity={identity}; HttpOnly; SameSite=Lax; Path=/")
                return
            if path == "/logout":
                self._redirect("demo_identity=; Max-Age=0; HttpOnly; SameSite=Lax; Path=/")
                return

            identity = self._identity()
            if path == "/":
                draft = store.get_for_owner(identity) if identity else None
                self._respond(HTTPStatus.OK, page(identity, draft["content"] if draft else ""))
                return

            if path.startswith("/api/drafts/"):
                if identity is None:
                    self._respond(HTTPStatus.UNAUTHORIZED, b'{"error":"choose a test identity"}', "application/json")
                    return
                draft_id = path.removeprefix("/api/drafts/")
                draft = store.get_by_id(draft_id)
                if draft is None:
                    self._respond(HTTPStatus.NOT_FOUND, b'{"error":"not found"}', "application/json")
                    return
                # EXERCISE DEFECT: any selected identity can read the author's draft.
                body = json.dumps({"id": draft["id"], "content": draft["content"]}).encode()
                self._respond(HTTPStatus.OK, body, "application/json")
                return

            self._respond(HTTPStatus.NOT_FOUND, b"Not found", "text/plain; charset=utf-8")

        def do_POST(self):
            path = urlparse(self.path).path
            if path != "/save":
                self._respond(HTTPStatus.NOT_FOUND, b"Not found", "text/plain; charset=utf-8")
                return
            identity = self._identity()
            if identity is None:
                self._respond(HTTPStatus.UNAUTHORIZED, b"Choose a test identity first", "text/plain; charset=utf-8")
                return
            length = int(self.headers.get("Content-Length", "0"))
            if length > 20_000:
                self._respond(HTTPStatus.REQUEST_ENTITY_TOO_LARGE, b"Draft too long", "text/plain; charset=utf-8")
                return
            form = parse_qs(self.rfile.read(length).decode("utf-8"), keep_blank_values=True)
            _text = form.get("text", [""])[0]
            _simulate_failure = form.get("simulate_failure", [""])[0] == "1"
            try:
                store.save(identity, _text, _simulate_failure)
            except SimulatedSaveFailure:
                # EXERCISE DEFECT: the write failure is swallowed and reported as success.
                pass
            # EXERCISE DEFECT: this response can claim success without a write.
            # It also discards unsaved text instead of offering Retry.
            self._respond(HTTPStatus.OK, page(identity, "", "Saved"))

    return ThreadingHTTPServer(("127.0.0.1", port), Handler)


def main():
    parser = argparse.ArgumentParser(description="Run the fictional local Save draft exercise")
    parser.add_argument("--port", type=int, default=8766)
    parser.add_argument("--db", type=Path, default=Path(__file__).with_name("sandbox.sqlite3"))
    args = parser.parse_args()
    server = create_server(args.port, args.db)
    print(f"Exercise running at http://127.0.0.1:{server.server_port}/ (Ctrl-C to stop)")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        server.server_close()


if __name__ == "__main__":
    main()
