"""
PKOS — app.py  |  Final (Installments 1-6 complete)

Routes:
  GET  /              → three-selves interface
  POST /add           → add item to a self's queue
  POST /resolve       → mark item resolved
  POST /audit         → run gap detection (on demand)
  GET  /ingest        → import text or CSV
  POST /ingest        → parse uploaded content, show for review
  POST /ingest/save   → save selected items to a self
  GET  /pk-gate       → passphrase gate
  GET  /first-run     → browser setup (disappears after first use)
  GET  /logout        → clear session
  GET  /robots.txt    → disallow all crawlers
  GET  /manifest.json → PWA manifest
  GET  /sw.js         → service worker (offline)
  GET  /icon.svg      → app icon
"""

import csv
import io
import json
import pathlib
import secrets
import time
import uuid
from datetime import date, datetime, timedelta
from functools import wraps

from flask import (Flask, abort, make_response, redirect, render_template_string,
                   request, session, url_for)
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from werkzeug.security import check_password_hash, generate_password_hash

# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------

BASE_DIR  = pathlib.Path(__file__).parent.resolve()
CFG_PATH  = BASE_DIR / "pkos_config.json"
DATA_PATH = BASE_DIR / "data.json"

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------

def _load_cfg():
    if CFG_PATH.exists():
        return json.loads(CFG_PATH.read_text(encoding="utf-8"))
    return None

def _save_cfg(passphrase: str) -> dict:
    cfg = {
        "secret_key":      secrets.token_hex(32),
        "passphrase_hash": generate_password_hash(
                               passphrase, method="pbkdf2:sha256", salt_length=16),
        "gate_path":       "/pk-gate",
    }
    CFG_PATH.write_text(json.dumps(cfg, indent=2), encoding="utf-8")
    try:
        restart = BASE_DIR / "tmp" / "restart.txt"
        restart.parent.mkdir(exist_ok=True)
        restart.touch()
    except Exception:
        pass
    return cfg

def is_configured() -> bool:
    return CFG_PATH.exists()

# ---------------------------------------------------------------------------
# App
# ---------------------------------------------------------------------------

_cfg = _load_cfg()
app  = Flask(__name__)
app.secret_key                        = _cfg["secret_key"] if _cfg else secrets.token_hex(32)
app.permanent_session_lifetime        = timedelta(days=7)
app.config["SESSION_COOKIE_HTTPONLY"] = True
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
app.config["MAX_CONTENT_LENGTH"]      = 5 * 1024 * 1024   # 5 MB max upload

# ---------------------------------------------------------------------------
# Rate limiter
# ---------------------------------------------------------------------------

limiter = Limiter(
    key_func=get_remote_address,
    app=app,
    default_limits=[],
    storage_uri="memory://"
)

# ---------------------------------------------------------------------------
# Bot blocklist
# ---------------------------------------------------------------------------

_BOTS = [
    "bot","crawl","spider","scrape","curl","wget",
    "python-requests","python-urllib","go-http","java/",
    "libwww","httpclient","okhttp","axios","mechanize",
    "scrapy","heritrix","nutch","yandex","semrush","ahrefs",
    "mj12bot","dotbot","petalbot","gptbot","claudebot",
    "anthropic-ai","openai","facebookexternalhit","twitterbot",
    "linkedinbot","slurp","ia_archiver","archive.org",
    "bingbot","googlebot","duckduckbot",
]

def _is_bot(ua: str) -> bool:
    return any(f in ua.lower() for f in _BOTS)

@app.before_request
def _block_bots():
    if request.path == "/robots.txt":
        return
    if _is_bot(request.headers.get("User-Agent", "")):
        abort(404)

@app.before_request
def _enforce_setup():
    if not is_configured():
        if request.endpoint not in ("first_run", "robots_txt", "static"):
            return redirect(url_for("first_run"))

@app.after_request
def _security_headers(resp):
    resp.headers["X-Robots-Tag"]           = "noindex, nofollow"
    resp.headers["X-Content-Type-Options"] = "nosniff"
    resp.headers["X-Frame-Options"]        = "DENY"
    resp.headers["Referrer-Policy"]        = "no-referrer"
    resp.headers["Cache-Control"]          = "no-store, no-cache, must-revalidate"
    return resp

# ---------------------------------------------------------------------------
# CSRF
# ---------------------------------------------------------------------------

def _csrf_token() -> str:
    if "_csrf" not in session:
        session["_csrf"] = secrets.token_hex(32)
    return session["_csrf"]

def _csrf_valid(token: str) -> bool:
    return bool(token) and token == session.get("_csrf")

# ---------------------------------------------------------------------------
# Auth
# ---------------------------------------------------------------------------

def login_required(f):
    @wraps(f)
    def _inner(*a, **kw):
        if not session.get("authenticated"):
            return redirect(url_for("gate"))
        return f(*a, **kw)
    return _inner

# ---------------------------------------------------------------------------
# Data helpers
# ---------------------------------------------------------------------------

def _load_data() -> dict:
    return json.loads(DATA_PATH.read_text(encoding="utf-8"))

def _save_data(data: dict):
    DATA_PATH.write_text(
        json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")

def _now() -> str:
    return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")

def _today() -> str:
    return date.today().isoformat()

def _fmt_date(iso: str) -> str:
    try:
        return datetime.fromisoformat(iso[:10]).strftime("%-d %b %Y")
    except Exception:
        return iso

def _days_until(iso_date: str):
    try:
        return (date.fromisoformat(iso_date) - date.today()).days
    except Exception:
        return None

def _open_items(data: dict, self_key: str) -> list:
    order = {"high": 0, "medium": 1, "low": 2}
    items = [i for i in data[self_key]["items"] if i["status"] == "open"]
    items.sort(key=lambda i: (order.get(i.get("priority","low"), 2), i.get("created","")))
    return items

def _open_deadlines(data: dict, self_key: str) -> list:
    """Return open deadlines sorted by due date ascending."""
    dl = [i for i in data[self_key]["deadlines"] if i["status"] == "open"]
    dl.sort(key=lambda i: i.get("due") or "9999")
    return dl

def _next_deadline(data: dict, self_key: str):
    for dl in _open_deadlines(data, self_key):
        d = _days_until(dl.get("due",""))
        if d is not None and d >= 0:
            return {"item": dl, "days": d}
    return None

def _make_item(self_key, text, priority="medium", item_type="item", due=None) -> dict:
    return {
        "id":       str(uuid.uuid4()),
        "created":  _now(),
        "type":     item_type,
        "status":   "open",
        "priority": priority if priority in ("high","medium","low") else "medium",
        "text":     text.strip(),
        "notes":    None,
        "due":      due or None,
        "self":     self_key,
    }

def _append_item(data: dict, self_key: str, item: dict):
    key = "deadlines" if item["type"] == "deadline" else "items"
    data[self_key][key].append(item)
    if "audit_log" not in data:
        data["audit_log"] = []
    data["audit_log"].append({
        "timestamp": _now(),
        "action":    "created",
        "item_id":   item["id"],
        "previous":  None,
    })
    data["meta"]["last_modified"] = _today()

# ---------------------------------------------------------------------------
# Installment 4 — Deadline engine
# Greek teacher administrative calendar (approximate — confirm with accountant)
# ---------------------------------------------------------------------------

def _suggested_deadlines() -> list:
    """
    Return upcoming suggested administrative deadlines for a Greek
    language teacher. Dates are approximate for the current year.
    The app clearly marks these as needing accountant confirmation.
    """
    yr    = date.today().year
    today = date.today()
    raw   = [
        {
            "text": f"Φορολογική Δήλωση {yr} — income tax declaration",
            "due":  f"{yr}-06-30",
            "note": "Approximate date — confirm with your accountant",
        },
        {
            "text": f"ΕΦΚΑ contributions Q2 {yr}",
            "due":  f"{yr}-07-31",
            "note": "Confirm amount and exact date with your accountant",
        },
        {
            "text": f"ΕΝΦΙΑ {yr} — property tax",
            "due":  f"{yr}-09-30",
            "note": "Only if you own property in Greece",
        },
        {
            "text": f"ΕΦΚΑ contributions Q3 {yr}",
            "due":  f"{yr}-10-31",
            "note": "Confirm amount and exact date with your accountant",
        },
        {
            "text": f"Φορολογική Δήλωση {yr+1} period opens",
            "due":  f"{yr+1}-04-01",
            "note": "Approximate — confirm with your accountant",
        },
    ]
    return [s for s in raw if date.fromisoformat(s["due"]) >= today]

# ---------------------------------------------------------------------------
# Installment 6 — Gap detection / audit logic
# ---------------------------------------------------------------------------

_INCOME_KW  = ["paid","payment","income","kofi","italki","transfer",
                "deposit","fee","πληρωμ","εισόδημα","received"]
_LESSON_KW  = ["lesson","session","student","taught","class","teaching",
                "μάθημα","μαθητ","taught","hour","slot","call","zoom"]

def _classify(text: str):
    t = text.lower()
    is_income = any(k in t for k in _INCOME_KW)
    is_lesson = any(k in t for k in _LESSON_KW)
    if is_income and not is_lesson:
        return "income"
    if is_lesson and not is_income:
        return "lesson"
    return "other"

def _run_audit(data: dict) -> dict:
    """
    Scan the Professional self for:
      Ghost Income  — a payment entry with no lesson within 7 days either side
      Ghost Lesson  — a lesson entry with no payment within 7 days either side
      Black Holes   — gaps of 30+ days with no professional data at all

    Returns {"flags": [...new flag items...], "summary": {...counts...}}
    All findings require human confirmation — nothing is auto-resolved.
    """
    all_pro = [i for i in data["professional"]["items"]
               if i["status"] in ("open","resolved")]

    income_items = [i for i in all_pro if _classify(i["text"]) == "income"]
    lesson_items = [i for i in all_pro if _classify(i["text"]) == "lesson"]

    flags = []

    def _item_date(item):
        try:
            return date.fromisoformat(item.get("created","")[:10])
        except Exception:
            return None

    # Ghost Income
    for inc in income_items:
        d = _item_date(inc)
        if not d:
            continue
        matched = any(
            abs((_item_date(l) - d).days) <= 7
            for l in lesson_items
            if _item_date(l)
        )
        if not matched:
            flags.append(_make_item(
                "professional",
                f"Ghost Income: payment on {_fmt_date(inc['created'][:10])} "
                f"has no matching lesson — \"{inc['text'][:80]}\"",
                priority="high",
                item_type="flag",
            ))

    # Ghost Lesson
    for les in lesson_items:
        d = _item_date(les)
        if not d:
            continue
        matched = any(
            abs((_item_date(i) - d).days) <= 7
            for i in income_items
            if _item_date(i)
        )
        if not matched:
            flags.append(_make_item(
                "professional",
                f"Ghost Lesson: session on {_fmt_date(les['created'][:10])} "
                f"has no matching payment — \"{les['text'][:80]}\"",
                priority="high",
                item_type="flag",
            ))

    # Black Holes
    all_dates = sorted(
        filter(None, (_item_date(i) for i in all_pro))
    )
    if len(all_dates) >= 2:
        for i in range(len(all_dates) - 1):
            gap = (all_dates[i+1] - all_dates[i]).days
            if gap >= 30:
                flags.append(_make_item(
                    "professional",
                    f"Black hole: {gap} days with no professional entries "
                    f"({_fmt_date(all_dates[i].isoformat())} → "
                    f"{_fmt_date(all_dates[i+1].isoformat())})",
                    priority="medium",
                    item_type="flag",
                ))

    summary = {
        "ghost_income":  sum(1 for f in flags if "Ghost Income" in f["text"]),
        "ghost_lessons": sum(1 for f in flags if "Ghost Lesson" in f["text"]),
        "black_holes":   sum(1 for f in flags if "Black hole"   in f["text"]),
        "total":         len(flags),
    }
    return {"flags": flags, "summary": summary}

# ---------------------------------------------------------------------------
# Installment 5 — File ingestion helpers
# ---------------------------------------------------------------------------

def _parse_text(raw: str) -> list:
    """Split plain text into non-empty lines, strip each."""
    lines = [l.strip() for l in raw.splitlines()]
    return [l for l in lines if l]

def _parse_csv(raw: str) -> list:
    """Parse CSV, join each row into a readable string."""
    rows = []
    reader = csv.reader(io.StringIO(raw))
    for row in reader:
        joined = " | ".join(cell.strip() for cell in row if cell.strip())
        if joined:
            rows.append(joined)
    return rows

# ---------------------------------------------------------------------------
# Routes — robots, PWA
# ---------------------------------------------------------------------------

@app.route("/robots.txt")
def robots_txt():
    r = make_response("User-agent: *\nDisallow: /\n")
    r.headers["Content-Type"] = "text/plain"
    return r

@app.route("/manifest.json")
def manifest():
    data = {
        "name": "PKOS", "short_name": "PKOS",
        "description": "Three selves, one secretary.",
        "start_url": "/pk-gate",
        "display": "standalone",
        "background_color": "#18181c",
        "theme_color": "#18181c",
        "icons": [{"src": "/icon.svg", "sizes": "any", "type": "image/svg+xml"}],
    }
    r = make_response(json.dumps(data))
    r.headers["Content-Type"] = "application/manifest+json"
    r.headers["Cache-Control"] = "public, max-age=86400"
    return r

@app.route("/icon.svg")
def icon_svg():
    svg = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  <rect width="512" height="512" rx="80" fill="#18181c"/>
  <rect x="32" y="32" width="448" height="448" rx="60" fill="#1e1e23"/>
  <text x="256" y="320" font-family="system-ui,sans-serif" font-size="220"
        font-weight="300" fill="#5a6e96" text-anchor="middle"
        letter-spacing="-8">PK</text>
  <line x1="80" y1="380" x2="432" y2="380" stroke="#2e2e36" stroke-width="2"/>
  <rect x="80"  y="394" width="80" height="3" rx="1.5" fill="#5a6e96"/>
  <rect x="175" y="394" width="55" height="3" rx="1.5" fill="#8b5e52"/>
  <rect x="245" y="394" width="55" height="3" rx="1.5" fill="#5e7a5e"/>
</svg>"""
    r = make_response(svg)
    r.headers["Content-Type"] = "image/svg+xml"
    r.headers["Cache-Control"] = "public, max-age=604800"
    return r

@app.route("/sw.js")
def service_worker():
    js = r"""
const CACHE = 'pkos-v1';
const PRECACHE = ['/', '/pk-gate', '/icon.svg'];
self.addEventListener('install', e => {
  e.waitUntil(caches.open(CACHE).then(c => c.addAll(PRECACHE)).then(() => self.skipWaiting()));
});
self.addEventListener('activate', e => {
  e.waitUntil(
    caches.keys()
      .then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k))))
      .then(() => self.clients.claim())
  );
});
self.addEventListener('fetch', e => {
  if (e.request.method !== 'GET') return;
  const p = new URL(e.request.url).pathname;
  if (p === '/manifest.json' || p === '/icon.svg') {
    e.respondWith(caches.match(e.request).then(c => c || fetch(e.request)));
    return;
  }
  e.respondWith(
    fetch(e.request)
      .then(r => {
        if (r.ok) {
          const cl = r.clone();
          caches.open(CACHE).then(c => c.put(e.request, cl));
        }
        return r;
      })
      .catch(() => caches.match(e.request).then(c => c || new Response(
        '<html><body style="background:#18181c;color:#55535a;font-family:system-ui;' +
        'display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0">' +
        '<div style="text-align:center;font-size:.8rem;letter-spacing:.2em">PKOS &mdash; offline</div>' +
        '</body></html>',
        {headers:{'Content-Type':'text/html'}}
      )))
  );
});
"""
    r = make_response(js.strip())
    r.headers["Content-Type"] = "application/javascript"
    r.headers["Cache-Control"] = "no-cache"
    r.headers["Service-Worker-Allowed"] = "/"
    return r

# ---------------------------------------------------------------------------
# Routes — setup & auth
# ---------------------------------------------------------------------------

@app.route("/first-run", methods=["GET","POST"])
def first_run():
    if is_configured():
        return redirect(url_for("gate"))
    token = _csrf_token()
    error = None
    if request.method == "POST":
        if request.form.get("address","") != "":
            time.sleep(2); return redirect(url_for("first_run"))
        if not _csrf_valid(request.form.get("_t","")):
            error = "Something went wrong. Try again."
        else:
            phrase  = request.form.get("phrase","").strip()
            confirm = request.form.get("phrase2","").strip()
            if len(phrase) < 6:
                error = "Too short. Use four or more words."
            elif phrase != confirm:
                error = "The two passphrases do not match."
            else:
                _save_cfg(phrase)
                return render_template_string(_SETUP_DONE)
    return render_template_string(_SETUP, token=token, error=error)

@app.route("/pk-gate", methods=["GET","POST"])
@limiter.limit("5 per 15 minutes", error_message="")
def gate():
    if not is_configured():
        return redirect(url_for("first_run"))
    cfg   = _load_cfg()
    token = _csrf_token()
    error = False
    if request.method == "POST":
        if request.form.get("address","") != "":
            time.sleep(2); return redirect(url_for("gate"))
        if not _csrf_valid(request.form.get("_t","")):
            time.sleep(2); error = True
        else:
            phrase = request.form.get("phrase","")
            if check_password_hash(cfg["passphrase_hash"], phrase):
                session.permanent = True
                session["authenticated"] = True
                session.pop("_csrf", None)
                return redirect(url_for("index"))
            else:
                time.sleep(2); error = True
    return render_template_string(_GATE, token=token, error=error)

@app.route("/logout")
@login_required
def logout():
    session.clear()
    return redirect(url_for("gate"))

# ---------------------------------------------------------------------------
# Routes — main app
# ---------------------------------------------------------------------------

@app.route("/")
@login_required
def index():
    data      = _load_data()
    token     = _csrf_token()
    pro       = _open_items(data, "professional")
    house     = _open_items(data, "householder")
    sov       = _open_items(data, "sovereign")
    pro_dl    = _open_deadlines(data, "professional")
    house_dl  = _open_deadlines(data, "householder")
    sov_dl    = _open_deadlines(data, "sovereign")
    next_pro  = _next_deadline(data, "professional")
    next_house= _next_deadline(data, "householder")
    next_sov  = _next_deadline(data, "sovereign")
    suggested = _suggested_deadlines()
    audit_msg = session.pop("audit_msg", None)
    return render_template_string(
        _INDEX,
        token=token,
        pro=pro,       pro_dl=pro_dl,    next_pro=next_pro,
        house=house,   house_dl=house_dl, next_house=next_house,
        sov=sov,       sov_dl=sov_dl,    next_sov=next_sov,
        suggested=suggested,
        audit_msg=audit_msg,
        fmt=_fmt_date,
        days_until=_days_until,
    )

@app.route("/add", methods=["POST"])
@login_required
def add_item():
    if not _csrf_valid(request.form.get("_t","")): abort(403)
    self_key = request.form.get("self","")
    if self_key not in ("professional","householder","sovereign"): abort(400)
    text = request.form.get("text","").strip()
    if not text: return redirect(url_for("index"))
    item = _make_item(
        self_key, text,
        priority  = request.form.get("priority","medium"),
        item_type = request.form.get("item_type","item"),
        due       = request.form.get("due","").strip() or None,
    )
    data = _load_data()
    _append_item(data, self_key, item)
    _save_data(data)
    return redirect(url_for("index"))

@app.route("/resolve", methods=["POST"])
@login_required
def resolve_item():
    if not _csrf_valid(request.form.get("_t","")): abort(403)
    item_id  = request.form.get("id","")
    self_key = request.form.get("self","")
    if self_key not in ("professional","householder","sovereign"):
        return redirect(url_for("index"))
    data = _load_data()
    for pool in ("items","deadlines"):
        for item in data[self_key][pool]:
            if item["id"] == item_id and item["status"] == "open":
                prev = dict(item)
                item["status"]   = "resolved"
                item["resolved"] = _now()
                if "audit_log" not in data:
                    data["audit_log"] = []
                data["audit_log"].append({
                    "timestamp": _now(), "action": "resolved",
                    "item_id": item_id, "previous": prev,
                })
                data["meta"]["last_modified"] = _today()
                break
    _save_data(data)
    return redirect(url_for("index"))

# ---------------------------------------------------------------------------
# Route — audit (Installment 6)
# ---------------------------------------------------------------------------

@app.route("/audit", methods=["POST"])
@login_required
def audit():
    if not _csrf_valid(request.form.get("_t","")): abort(403)
    data   = _load_data()
    result = _run_audit(data)
    for flag in result["flags"]:
        _append_item(data, "professional", flag)
    _save_data(data)
    s = result["summary"]
    msg = (f"Audit complete — {s['total']} issue(s) found: "
           f"{s['ghost_income']} ghost income, "
           f"{s['ghost_lessons']} ghost lesson(s), "
           f"{s['black_holes']} black hole(s). "
           f"Review the flags in your Professional panel.")
    if s["total"] == 0:
        msg = "Audit complete — no obvious gaps detected in the current data."
    session["audit_msg"] = msg
    return redirect(url_for("index"))

# ---------------------------------------------------------------------------
# Route — add suggested deadline (Installment 4)
# ---------------------------------------------------------------------------

@app.route("/add-suggestion", methods=["POST"])
@login_required
def add_suggestion():
    if not _csrf_valid(request.form.get("_t","")): abort(403)
    text = request.form.get("text","").strip()
    due  = request.form.get("due","").strip()
    if not text: return redirect(url_for("index"))
    item = _make_item("professional", text, priority="medium",
                      item_type="deadline", due=due or None)
    data = _load_data()
    _append_item(data, "professional", item)
    _save_data(data)
    return redirect(url_for("index"))

# ---------------------------------------------------------------------------
# Routes — file ingestion (Installment 5)
# ---------------------------------------------------------------------------

@app.route("/ingest", methods=["GET","POST"])
@login_required
def ingest():
    token  = _csrf_token()
    parsed = None
    error  = None

    if request.method == "POST":
        if not _csrf_valid(request.form.get("_t","")): abort(403)

        raw = ""
        f   = request.files.get("file")

        if f and f.filename:
            name = f.filename.lower()
            if not (name.endswith(".txt") or name.endswith(".csv")):
                error = "Only .txt and .csv files are supported."
            else:
                try:
                    raw = f.read().decode("utf-8", errors="replace")
                except Exception:
                    error = "Could not read the file. Check it is plain text or CSV."
        else:
            raw = request.form.get("paste","").strip()

        if raw and not error:
            name = (f.filename.lower() if f and f.filename else "")
            if name.endswith(".csv"):
                lines = _parse_csv(raw)
            else:
                lines = _parse_text(raw)

            if not lines:
                error = "Nothing readable found in the file or text."
            else:
                parsed = lines

    return render_template_string(
        _INGEST, token=token, parsed=parsed, error=error)

@app.route("/ingest/save", methods=["POST"])
@login_required
def ingest_save():
    if not _csrf_valid(request.form.get("_t","")): abort(403)
    self_key = request.form.get("self","professional")
    if self_key not in ("professional","householder","sovereign"):
        self_key = "professional"
    priority = request.form.get("priority","medium")
    selected = request.form.getlist("selected")
    if not selected:
        return redirect(url_for("ingest"))
    data  = _load_data()
    count = 0
    for text in selected:
        text = text.strip()
        if not text:
            continue
        item = _make_item(self_key, text, priority=priority)
        _append_item(data, self_key, item)
        count += 1
    _save_data(data)
    session["audit_msg"] = f"{count} item(s) imported into {self_key.title()}."
    return redirect(url_for("index"))

# ---------------------------------------------------------------------------
# Error handlers
# ---------------------------------------------------------------------------

@app.errorhandler(429)
def _too_many(e):
    time.sleep(2)
    return render_template_string(_GATE, token=_csrf_token(), error=True), 200

@app.errorhandler(404)
def _not_found(e):  return "", 404

@app.errorhandler(403)
def _forbidden(e):  return "", 404

# ---------------------------------------------------------------------------
# CSS
# ---------------------------------------------------------------------------

_CSS = """
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
  --bg:         #18181c;
  --bg-card:    #1e1e23;
  --bg-input:   #222228;
  --border:     #2e2e36;
  --text:       #e2dfd8;
  --muted:      #55535a;
  --dim:        #38363e;
  --pro:        #5a6e96;
  --pro-soft:   #2a3248;
  --house:      #8b5e52;
  --house-soft: #3a2820;
  --sov:        #5e7a5e;
  --sov-soft:   #243024;
  --high:       #a05050;
  --medium:     #8a7a40;
  --low:        #3a3845;
  --danger:     #8b3a3a;
  --notice-bg:  #1e2030;
}
html,body { background:var(--bg); color:var(--text);
  font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
  font-size:15px; line-height:1.5; min-height:100vh; }

/* Header */
.app-header {
  display:flex; align-items:center; justify-content:space-between;
  padding:.85rem 1.5rem; border-bottom:1px solid var(--border);
  gap:1rem;
}
.logo { font-size:.75rem; letter-spacing:.5em; text-transform:uppercase; color:var(--muted); }
.header-links { display:flex; gap:1.2rem; align-items:center; }
.header-link {
  font-size:.7rem; letter-spacing:.12em; text-transform:uppercase;
  color:var(--dim); text-decoration:none; transition:color .15s;
}
.header-link:hover { color:var(--muted); }

/* Panels */
.panels { display:grid; grid-template-columns:1.3fr 1fr 1fr;
          min-height:calc(100vh - 50px); align-items:start; }
.panel { border-right:1px solid var(--border); padding:1.4rem 1.2rem 2.5rem;
         min-height:calc(100vh - 50px); }
.panel:last-child { border-right:none; }

.panel-header { display:flex; align-items:baseline; justify-content:space-between;
                margin-bottom:1.3rem; padding-bottom:.8rem; border-bottom:2px solid var(--border); }
.panel--pro   .panel-header { border-bottom-color:var(--pro); }
.panel--house .panel-header { border-bottom-color:var(--house); }
.panel--sov   .panel-header { border-bottom-color:var(--sov); }

.panel-name { font-size:.7rem; letter-spacing:.22em; text-transform:uppercase; }
.panel--pro   .panel-name { color:var(--pro); }
.panel--house .panel-name { color:var(--house); }
.panel--sov   .panel-name { color:var(--sov); }

.panel-meta { display:flex; gap:.6rem; align-items:center; }
.panel-count { font-size:.66rem; color:var(--muted); }

/* Audit button */
.audit-btn {
  background:none; border:1px solid var(--border); border-radius:3px;
  padding:.15rem .45rem; font-size:.64rem; letter-spacing:.08em;
  color:var(--dim); cursor:pointer; transition:all .15s; white-space:nowrap;
}
.audit-btn:hover { border-color:var(--pro); color:var(--pro); }

/* Audit message */
.audit-notice {
  font-size:.76rem; padding:.55rem .8rem; border-radius:5px;
  margin-bottom:1rem; background:var(--notice-bg);
  border-left:2px solid var(--pro); color:var(--muted); line-height:1.5;
}

/* Deadline notice */
.dl-notice { font-size:.74rem; padding:.5rem .75rem; border-radius:5px; margin-bottom:1rem; line-height:1.5; }
.dl-notice.urgent   { background:#2a1a1a; border-left:2px solid var(--danger); color:#c09090; }
.dl-notice.upcoming { background:var(--notice-bg); border-left:2px solid var(--dim); color:var(--muted); }

/* Deadlines list */
.deadlines-section { margin-bottom:1.2rem; }
.dl-label { font-size:.64rem; letter-spacing:.15em; text-transform:uppercase;
            color:var(--dim); margin-bottom:.5rem; }
.dl-item { display:flex; align-items:baseline; justify-content:space-between;
           gap:.5rem; padding:.35rem 0; border-bottom:1px solid var(--border);
           font-size:.8rem; }
.dl-item:last-child { border-bottom:none; }
.dl-text { flex:1; color:var(--text); }
.dl-days { font-size:.68rem; white-space:nowrap; flex-shrink:0; }
.dl-days.overdue { color:var(--high); }
.dl-days.soon    { color:#a08040; }
.dl-days.ok      { color:var(--muted); }

/* Suggested deadlines */
.suggested-section { margin-bottom:1.4rem; }
.suggested-item {
  display:flex; align-items:center; justify-content:space-between;
  gap:.5rem; padding:.4rem 0; border-bottom:1px solid var(--border);
  font-size:.76rem;
}
.suggested-item:last-child { border-bottom:none; }
.suggested-text { flex:1; color:var(--muted); }
.suggested-note { font-size:.64rem; color:var(--dim); margin-top:.15rem; }
.add-dl-btn {
  background:none; border:1px solid var(--border); border-radius:3px;
  padding:.2rem .45rem; font-size:.64rem; color:var(--dim);
  cursor:pointer; white-space:nowrap; transition:all .15s; flex-shrink:0;
}
.add-dl-btn:hover { border-color:var(--pro); color:var(--pro); }

/* Items */
.items { margin-bottom:1.2rem; }
.item { display:flex; align-items:flex-start; gap:.6rem;
        padding:.65rem 0; border-bottom:1px solid var(--border); }
.item:last-child { border-bottom:none; }

.pdot { flex-shrink:0; width:7px; height:7px; border-radius:50%; margin-top:.45rem; }
.pdot.high   { background:var(--high); }
.pdot.medium { background:var(--medium); }
.pdot.low    { background:var(--low); }
.pdot.flag   { background:var(--danger); }

.item-body { flex:1; min-width:0; }
.item-text { font-size:.86rem; color:var(--text); line-height:1.45; word-break:break-word; }
.item-meta { font-size:.66rem; color:var(--dim); margin-top:.15rem; }
.item-due  { font-size:.66rem; color:var(--muted); margin-top:.1rem; }
.item-due.overdue { color:var(--high); }

.resolve-form { flex-shrink:0; }
.resolve-btn {
  background:none; border:1px solid var(--border); border-radius:4px;
  padding:.18rem .45rem; font-size:.66rem; letter-spacing:.06em;
  color:var(--dim); cursor:pointer; transition:all .15s; white-space:nowrap;
}
.resolve-btn:hover   { border-color:var(--muted); color:var(--muted); }
.resolve-btn.confirm { border-color:var(--sov); color:var(--sov); }

.empty { font-size:.76rem; color:var(--dim); padding:.4rem 0 .8rem; font-style:italic; }

/* Add form */
.add-section { margin-top:.6rem; }
.add-toggle {
  background:none; border:1px dashed var(--border); border-radius:5px;
  padding:.4rem .8rem; font-size:.72rem; letter-spacing:.1em;
  color:var(--dim); cursor:pointer; width:100%; text-align:center; transition:all .15s;
}
.panel--pro   .add-toggle:hover { border-color:var(--pro);   color:var(--pro); }
.panel--house .add-toggle:hover { border-color:var(--house); color:var(--house); }
.panel--sov   .add-toggle:hover { border-color:var(--sov);   color:var(--sov); }

.add-form {
  display:none; margin-top:.7rem; background:var(--bg-card);
  border:1px solid var(--border); border-radius:6px; padding:.85rem;
}
.add-form.open { display:block; }
.add-form textarea {
  display:block; width:100%; background:var(--bg-input);
  border:1px solid var(--border); border-radius:5px;
  padding:.55rem .75rem; color:var(--text); font-size:.84rem;
  font-family:inherit; resize:vertical; min-height:56px;
  outline:none; transition:border-color .15s;
}
.add-form textarea:focus    { border-color:var(--muted); }
.add-form textarea::placeholder { color:var(--dim); }

.add-row { display:flex; gap:.45rem; margin-top:.55rem; align-items:center; flex-wrap:wrap; }
.add-form select, .add-form input[type="date"] {
  background:var(--bg-input); border:1px solid var(--border); border-radius:5px;
  padding:.35rem .55rem; color:var(--text); font-size:.76rem; font-family:inherit; outline:none;
}
.type-row { display:flex; gap:.4rem; margin-bottom:.55rem; }
.type-btn {
  flex:1; background:var(--bg-input); border:1px solid var(--border);
  border-radius:4px; padding:.28rem; font-size:.7rem; letter-spacing:.06em;
  color:var(--dim); cursor:pointer; text-align:center; transition:all .15s;
}
.type-btn.active { border-color:var(--muted); color:var(--text); background:var(--border); }
.due-wrap { display:none; }
.add-submit {
  margin-left:auto; background:var(--bg-input); border:1px solid var(--border);
  border-radius:5px; padding:.35rem .85rem; color:var(--muted); font-size:.76rem;
  font-family:inherit; letter-spacing:.06em; cursor:pointer; transition:all .15s;
}
.add-submit:hover { border-color:var(--muted); color:var(--text); }

/* Ingest page */
.page-wrap { max-width:700px; margin:0 auto; padding:2rem 1.5rem; }
.page-title { font-size:.75rem; letter-spacing:.3em; text-transform:uppercase;
              color:var(--muted); margin-bottom:2rem; }
.back-link { font-size:.7rem; letter-spacing:.1em; color:var(--dim);
             text-decoration:none; display:inline-block; margin-bottom:1.5rem; }
.back-link:hover { color:var(--muted); }
.form-label { display:block; font-size:.7rem; letter-spacing:.12em;
              text-transform:uppercase; color:var(--muted); margin-bottom:.4rem; }
.form-note  { font-size:.72rem; color:var(--dim); margin-bottom:1.2rem; line-height:1.5; }
.ingest-area {
  width:100%; background:var(--bg-card); border:1px solid var(--border);
  border-radius:6px; padding:.75rem 1rem; color:var(--text); font-size:.86rem;
  font-family:inherit; resize:vertical; min-height:120px; outline:none;
  transition:border-color .15s; margin-bottom:1rem;
}
.ingest-area:focus { border-color:var(--muted); }
.file-input { font-size:.8rem; color:var(--muted); margin-bottom:1rem; display:block; }
.divider { text-align:center; font-size:.72rem; color:var(--dim);
           margin:1rem 0; letter-spacing:.1em; }
.ingest-submit {
  background:var(--pro-soft); border:1px solid var(--pro); border-radius:6px;
  padding:.6rem 1.4rem; color:#9aaecc; font-size:.8rem; font-family:inherit;
  letter-spacing:.12em; cursor:pointer; transition:all .15s;
}
.ingest-submit:hover { background:#2e3a52; color:#c0d0e8; }

.parsed-section { margin-top:2rem; }
.parsed-title { font-size:.7rem; letter-spacing:.2em; text-transform:uppercase;
                color:var(--muted); margin-bottom:1rem; }
.parsed-item { display:flex; gap:.75rem; align-items:flex-start;
               padding:.5rem 0; border-bottom:1px solid var(--border); }
.parsed-item:last-child { border-bottom:none; }
.parsed-check { margin-top:.2rem; flex-shrink:0; accent-color:var(--pro); }
.parsed-text { font-size:.84rem; color:var(--text); line-height:1.4; word-break:break-word; }
.parsed-controls { display:flex; gap:.5rem; margin-top:1.2rem; align-items:center; flex-wrap:wrap; }
.parsed-controls select {
  background:var(--bg-input); border:1px solid var(--border); border-radius:5px;
  padding:.4rem .65rem; color:var(--text); font-size:.78rem; font-family:inherit; outline:none;
}
.save-btn {
  background:var(--pro-soft); border:1px solid var(--pro); border-radius:5px;
  padding:.4rem 1rem; color:#9aaecc; font-size:.78rem; font-family:inherit;
  letter-spacing:.1em; cursor:pointer; transition:all .15s;
}
.save-btn:hover { background:#2e3a52; color:#c0d0e8; }
.select-all-btn {
  background:none; border:1px solid var(--border); border-radius:5px;
  padding:.4rem .75rem; color:var(--dim); font-size:.74rem;
  font-family:inherit; cursor:pointer; transition:all .15s;
}
.select-all-btn:hover { border-color:var(--muted); color:var(--muted); }

/* Gate / setup */
body.centered { display:flex; align-items:center; justify-content:center; }
.card { width:100%; max-width:360px; padding:2.5rem 2rem; }
.wordmark { font-size:.88rem; letter-spacing:.55em; color:var(--muted);
            text-align:center; text-transform:uppercase; margin-bottom:2.8rem; }
.error-bar { background:#2a1a1a; border-left:2px solid #7a3535; padding:.6rem .9rem;
             font-size:.8rem; color:#b07070; border-radius:0 4px 4px 0; margin-bottom:1.2rem; }
.hp { position:absolute; left:-9999px; opacity:0; pointer-events:none; }
label.lbl { display:block; font-size:.72rem; letter-spacing:.12em; color:var(--muted);
            text-transform:uppercase; margin-bottom:.4rem; margin-top:1.1rem; }
.field { display:block; width:100%; background:var(--bg-input);
         border:1px solid var(--border); border-radius:6px;
         padding:.8rem 1rem; color:var(--text); font-size:.95rem;
         font-family:inherit; outline:none; transition:border-color .15s; }
.field:focus { border-color:var(--pro); }
.field::placeholder { color:var(--dim); }
.btn { margin-top:1.6rem; display:block; width:100%; background:#252d3e;
       border:1px solid #364060; border-radius:6px; padding:.8rem; color:#9aaecc;
       font-size:.82rem; font-family:inherit; letter-spacing:.18em;
       text-transform:uppercase; cursor:pointer; transition:all .15s; }
.btn:hover { background:#2e3a52; color:#c0d0e8; }
.note { font-size:.75rem; color:var(--dim); margin-top:1.6rem; line-height:1.7; text-align:center; }
.go { display:block; text-align:center; margin-top:2rem; color:var(--pro);
      text-decoration:none; font-size:.82rem; letter-spacing:.2em; text-transform:uppercase; }
.go:hover { color:#9aaecc; }
.done { text-align:center; color:var(--muted); font-size:.88rem; line-height:1.8; }

/* Responsive */
@media (max-width:820px) {
  .panels { grid-template-columns:1fr; }
  .panel { border-right:none; border-bottom:1px solid var(--border); min-height:auto; }
  .panel:last-child { border-bottom:none; }
}
"""

# ---------------------------------------------------------------------------
# JS
# ---------------------------------------------------------------------------

_JS = """
// Toggle add form
document.querySelectorAll('.add-toggle').forEach(btn => {
  btn.addEventListener('click', function() {
    const form = this.nextElementSibling;
    form.classList.toggle('open');
    this.textContent = form.classList.contains('open') ? '− cancel' : '+ add';
  });
});

// Item type toggle
document.querySelectorAll('.type-btn').forEach(btn => {
  btn.addEventListener('click', function() {
    const row  = this.closest('.type-row');
    const form = this.closest('form');
    row.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active'));
    this.classList.add('active');
    form.querySelector('[name="item_type"]').value = this.dataset.type;
    const dw = form.querySelector('.due-wrap');
    if (dw) dw.style.display = this.dataset.type === 'deadline' ? 'block' : 'none';
  });
});

// Resolve confirmation
document.querySelectorAll('.resolve-btn').forEach(btn => {
  btn.addEventListener('click', function(e) {
    if (!this.dataset.confirmed) {
      e.preventDefault();
      const orig = this.textContent;
      this.textContent = 'confirm?';
      this.classList.add('confirm');
      this.dataset.confirmed = '1';
      setTimeout(() => {
        if (this.dataset.confirmed) {
          this.textContent = orig;
          this.classList.remove('confirm');
          delete this.dataset.confirmed;
        }
      }, 4000);
    }
  });
});

// Service worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js', {scope:'/'}).catch(()=>{});
  });
}
"""

# ---------------------------------------------------------------------------
# Templates — gate & setup
# ---------------------------------------------------------------------------

_H = """<!DOCTYPE html><html lang="en"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="robots" content="noindex,nofollow">
<title>PKOS</title>
<style>""" + _CSS + """</style>
</head>"""

_SETUP = _H + """<body class="centered">
<div class="card">
  <div class="wordmark">PKOS &mdash; setup</div>
  {% if error %}<div class="error-bar">{{ error }}</div>{% endif %}
  <form method="POST" autocomplete="off" spellcheck="false">
    <input type="hidden" name="_t" value="{{ token }}">
    <input class="hp" type="text" name="address" tabindex="-1" autocomplete="off">
    <label class="lbl">choose your passphrase</label>
    <input class="field" type="password" name="phrase"
           placeholder="four or more words" autofocus autocomplete="new-password">
    <label class="lbl">confirm passphrase</label>
    <input class="field" type="password" name="phrase2"
           placeholder="same passphrase again" autocomplete="new-password">
    <button class="btn" type="submit">create pkos</button>
  </form>
  <p class="note">Write your passphrase on paper before continuing.<br>No recovery option exists.</p>
</div></body></html>"""

_SETUP_DONE = _H + """<body class="centered">
<div class="card">
  <div class="wordmark">PKOS</div>
  <p class="done">Setup complete.<br>Your passphrase is set.<br>This page will not appear again.</p>
  <a class="go" href="/pk-gate">enter &rarr;</a>
</div></body></html>"""

_GATE = _H + """<body class="centered">
<div class="card">
  <div class="wordmark">PKOS</div>
  {% if error %}<div class="error-bar">Passphrase not recognised.</div>{% endif %}
  <form method="POST" autocomplete="off" spellcheck="false">
    <input type="hidden" name="_t" value="{{ token }}">
    <input class="hp" type="text" name="address" tabindex="-1" autocomplete="off">
    <input class="field" type="password" name="phrase"
           placeholder="passphrase" autofocus
           autocomplete="new-password" autocorrect="off" autocapitalize="none">
    <button class="btn" type="submit">enter</button>
  </form>
</div></body></html>"""

# ---------------------------------------------------------------------------
# Main index template
# ---------------------------------------------------------------------------

_PANEL = """{% macro panel(key, label, cls, items, deadlines, next_dl, token) %}
<section class="panel panel--{{ cls }}">
  <div class="panel-header">
    <span class="panel-name">{{ label }}</span>
    <div class="panel-meta">
      <span class="panel-count">{{ items|length }} open</span>
      {% if key == 'professional' %}
      <form method="POST" action="/audit" style="display:inline">
        <input type="hidden" name="_t" value="{{ token }}">
        <button class="audit-btn" type="submit">audit</button>
      </form>
      {% endif %}
    </div>
  </div>

  {% if next_dl %}
    <div class="dl-notice {% if next_dl.days <= 7 %}urgent{% else %}upcoming{% endif %}">
      {% if next_dl.days == 0 %}Today{% else %}{{ next_dl.days }}d{% endif %}
      &mdash; {{ next_dl.item.text }}
    </div>
  {% endif %}

  {% if deadlines %}
  <div class="deadlines-section">
    <div class="dl-label">deadlines</div>
    {% for dl in deadlines %}
    {% set d = days_until(dl.due) if dl.due else none %}
    <div class="dl-item">
      <span class="dl-text">{{ dl.text }}</span>
      <span class="dl-days {% if d is not none and d < 0 %}overdue{% elif d is not none and d <= 14 %}soon{% else %}ok{% endif %}">
        {% if d is not none %}
          {% if d < 0 %}{{ d|abs }}d overdue
          {% elif d == 0 %}today
          {% else %}{{ d }}d
          {% endif %}
        {% endif %}
      </span>
      <form method="POST" action="/resolve">
        <input type="hidden" name="_t" value="{{ token }}">
        <input type="hidden" name="id" value="{{ dl.id }}">
        <input type="hidden" name="self" value="{{ key }}">
        <button type="submit" class="resolve-btn">done</button>
      </form>
    </div>
    {% endfor %}
  </div>
  {% endif %}

  <div class="items">
    {% if items %}
      {% for item in items %}
      <div class="item">
        <span class="pdot {{ 'flag' if item.type == 'flag' else item.priority }}"></span>
        <div class="item-body">
          <div class="item-text">{{ item.text }}</div>
          <div class="item-meta">{{ fmt(item.created[:10]) }}</div>
          {% if item.due %}
            {% set d = days_until(item.due) %}
            <div class="item-due {% if d is not none and d < 0 %}overdue{% endif %}">
              {% if d is not none %}
                {% if d < 0 %}overdue {{ d|abs }}d
                {% elif d == 0 %}due today
                {% else %}due in {{ d }}d
                {% endif %}
              {% endif %}
            </div>
          {% endif %}
        </div>
        <form class="resolve-form" method="POST" action="/resolve">
          <input type="hidden" name="_t" value="{{ token }}">
          <input type="hidden" name="id" value="{{ item.id }}">
          <input type="hidden" name="self" value="{{ key }}">
          <button type="submit" class="resolve-btn">resolve</button>
        </form>
      </div>
      {% endfor %}
    {% else %}
      <div class="empty">nothing open</div>
    {% endif %}
  </div>

  <div class="add-section">
    <button class="add-toggle">+ add</button>
    <form class="add-form" method="POST" action="/add" autocomplete="off">
      <input type="hidden" name="_t" value="{{ token }}">
      <input type="hidden" name="self" value="{{ key }}">
      <input type="hidden" name="item_type" value="item">
      <div class="type-row">
        <div class="type-btn active" data-type="item">item</div>
        <div class="type-btn" data-type="deadline">deadline</div>
      </div>
      <textarea name="text"
        placeholder="{% if key == 'professional' %}lesson with A., iTalki — unpaid{% elif key == 'householder' %}ΔΕΗ bill — check amount{% else %}Greek B2 — practice 30 min{% endif %}"
        rows="2"></textarea>
      <div class="add-row">
        <select name="priority">
          <option value="high">high</option>
          <option value="medium" selected>medium</option>
          <option value="low">low</option>
        </select>
        <div class="due-wrap">
          <input type="date" name="due">
        </div>
        <button type="submit" class="add-submit">add</button>
      </div>
    </form>
  </div>
</section>
{% endmacro %}"""

_INDEX = _H + """
<head>
  <meta name="theme-color" content="#18181c">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
  <meta name="apple-mobile-web-app-title" content="PKOS">
  <link rel="manifest" href="/manifest.json">
  <link rel="icon" href="/icon.svg" type="image/svg+xml">
</head>
<body>
""" + _PANEL + """
<div class="app-header">
  <span class="logo">PKOS</span>
  <div class="header-links">
    <a href="/ingest" class="header-link">import</a>
    <a href="/logout" class="header-link">exit</a>
  </div>
</div>

{% if audit_msg %}
<div style="padding:.5rem 1.5rem">
  <div class="audit-notice">{{ audit_msg }}</div>
</div>
{% endif %}

{% if suggested %}
<div style="padding:.4rem 1.5rem .2rem; border-bottom:1px solid var(--border)">
  <div style="display:flex; gap:1rem; align-items:baseline; flex-wrap:wrap">
    <span style="font-size:.64rem;letter-spacing:.2em;text-transform:uppercase;color:var(--dim)">
      suggested deadlines
    </span>
    {% for s in suggested %}
    <form method="POST" action="/add-suggestion" style="display:inline">
      <input type="hidden" name="_t" value="{{ token }}">
      <input type="hidden" name="text" value="{{ s.text }}">
      <input type="hidden" name="due"  value="{{ s.due }}">
      <button class="add-dl-btn" type="submit" title="{{ s.note }}">
        + {{ s.text[:45] }}{% if s.text|length > 45 %}…{% endif %}
      </button>
    </form>
    {% endfor %}
  </div>
</div>
{% endif %}

<div class="panels">
  {{ panel("professional", "Professional", "pro",   pro,   pro_dl,   next_pro,   token) }}
  {{ panel("householder",  "Householder",  "house", house, house_dl, next_house, token) }}
  {{ panel("sovereign",    "Sovereign",    "sov",   sov,   sov_dl,   next_sov,   token) }}
</div>

<script>""" + _JS + """</script>
</body></html>"""

# ---------------------------------------------------------------------------
# Ingest template (Installment 5)
# ---------------------------------------------------------------------------

_INGEST = _H + """<body>
<div class="page-wrap">
  <a href="/" class="back-link">&larr; back</a>
  <div class="page-title">Import text or CSV</div>

  <p class="form-note">
    Paste text (one item per line) or upload a .txt or .csv file.<br>
    Nothing is added automatically — you choose what to keep.
  </p>

  {% if error %}
  <div class="error-bar" style="margin-bottom:1.2rem">{{ error }}</div>
  {% endif %}

  <form method="POST" enctype="multipart/form-data" autocomplete="off">
    <input type="hidden" name="_t" value="{{ token }}">

    <label class="form-label">paste text</label>
    <textarea class="ingest-area" name="paste"
              placeholder="One item per line. Paste anything — lesson notes, bank statement lines, calendar entries."></textarea>

    <div class="divider">or</div>

    <label class="form-label">upload a file</label>
    <input class="file-input" type="file" name="file" accept=".txt,.csv">

    <br>
    <button class="ingest-submit" type="submit">parse &rarr;</button>
  </form>

  {% if parsed %}
  <div class="parsed-section">
    <div class="parsed-title">{{ parsed|length }} line(s) found — select what to import</div>

    <form method="POST" action="/ingest/save">
      <input type="hidden" name="_t" value="{{ token }}">

      {% for line in parsed %}
      <div class="parsed-item">
        <input class="parsed-check" type="checkbox" name="selected"
               value="{{ line }}" id="p{{ loop.index }}" checked>
        <label class="parsed-text" for="p{{ loop.index }}">{{ line }}</label>
      </div>
      {% endfor %}

      <div class="parsed-controls">
        <button type="button" class="select-all-btn" id="selAll">deselect all</button>
        <span style="font-size:.72rem;color:var(--dim)">→ add to</span>
        <select name="self">
          <option value="professional">Professional</option>
          <option value="householder">Householder</option>
          <option value="sovereign">Sovereign</option>
        </select>
        <select name="priority">
          <option value="medium" selected>medium priority</option>
          <option value="high">high priority</option>
          <option value="low">low priority</option>
        </select>
        <button class="save-btn" type="submit">add selected</button>
      </div>
    </form>
  </div>
  {% endif %}
</div>

<script>
const btn = document.getElementById('selAll');
if (btn) {
  let all = true;
  btn.addEventListener('click', () => {
    all = !all;
    document.querySelectorAll('.parsed-check').forEach(c => c.checked = all);
    btn.textContent = all ? 'deselect all' : 'select all';
  });
}
</script>
</body></html>"""

# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=False)
