"""Run: python app.py --demo, or set ODDS_API_KEY and run python app.py."""
from __future__ import annotations

import argparse
import json
import os
from pathlib import Path
import re
import time
from threading import BoundedSemaphore, Lock
from urllib.parse import urlsplit

from flask import Flask, Response, jsonify, render_template, request, stream_with_context
import requests

from racing_api import APIError, COUNTRIES, RACE_TYPES, DemoAPI, OddsState, RacingAPI


def create_app(api=None):
    app = Flask(__name__, root_path=str(Path(__file__).resolve().parent))
    app.config["TRUSTED_HOSTS"] = ["127.0.0.1", "localhost"]
    api = api or RacingAPI(os.getenv("ODDS_API_KEY", ""), os.getenv("ODDS_API_BASE_URL", "https://api.odds-api.net/v1"))
    cache, cache_lock = {}, Lock()
    # This is a local, single-viewer example. One active upstream stream per process.
    stream_slot = BoundedSemaphore(1)

    @app.before_request
    def local_requests_only():
        if urlsplit("http://" + request.host).hostname not in {"127.0.0.1", "localhost"}:
            return jsonify(error="Open the board directly on localhost."), 400
        origin = request.headers.get("Origin")
        if request.headers.get("Sec-Fetch-Site") == "cross-site" or (origin and urlsplit(origin).netloc != request.host):
            return jsonify(error="Open the board directly on localhost."), 403

    @app.after_request
    def headers(response):
        response.headers["Cache-Control"] = "no-store"
        response.headers["X-Content-Type-Options"] = "nosniff"
        response.headers["Referrer-Policy"] = "no-referrer"
        response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; frame-ancestors 'none'"
        return response

    @app.get("/")
    def index():
        return render_template("index.html", demo=api.demo, countries=COUNTRIES, race_types=RACE_TYPES)

    @app.errorhandler(APIError)
    def api_error(error):
        response = jsonify(error=str(error))
        response.status_code = error.status
        response.headers["Retry-After"] = str(error.retry_after)
        return response

    @app.get("/api/races")
    def races():
        country = request.args.get("country", "AU").upper()
        race_type = request.args.get("race_type", "horse-racing")
        if country not in COUNTRIES or race_type not in RACE_TYPES:
            return jsonify(error="Choose a supported country and race type."), 400
        key = country, race_type
        with cache_lock:
            saved = cache.get(key)
            if not saved or time.monotonic() - saved[0] > 60:
                saved = time.monotonic(), api.events(country, race_type)
                cache[key] = saved
            # Race lists are cached; a passed start must never become 'next to jump'.
            rows = [row for row in saved[1] if float(row["race_start_time"]) >= time.time()]
        fields = ("event_id", "race_venue", "race_number", "race_start_time", "race_country", "race_type", "race_distance", "status")
        return jsonify(items=[{k: row.get(k) for k in fields} for row in rows], demo=api.demo)

    @app.get("/api/races/<event_id>/stream")
    def odds_stream(event_id):
        if not re.fullmatch(r"[A-Za-z0-9_.:-]{1,240}", event_id):
            return jsonify(error="Invalid race identifier."), 400
        if not stream_slot.acquire(blocking=False):
            response = jsonify(error="Another race is still streaming. Close the other tab or wait a moment.")
            response.status_code = 429
            response.headers["Retry-After"] = "5"
            return response

        def message(name, data):
            return f"event: {name}\ndata: {json.dumps(data, separators=(',', ':'))}\n\n"

        def generate():
            try:
                # New connections always load a fresh snapshot before accepting deltas.
                # Do not reuse a cursor without its matching local state.
                while True:
                    state = OddsState(api.detail(event_id), api.snapshot(event_id))
                    detail_read_at = time.monotonic()
                    yield message("snapshot", state.view())
                    resync = False
                    for event, payload in api.stream(event_id, state.resume):
                        if event == "resync":
                            yield message("status", {"message": "Refreshing the racecard and price snapshot."})
                            resync = True
                            break
                        card_refreshed = time.monotonic() - detail_read_at >= 60
                        if card_refreshed:
                            state.detail = api.detail(event_id)
                            detail_read_at = time.monotonic()
                        if event == "delta":
                            state.apply(payload)
                            yield message("snapshot", state.view())
                        elif event == "heartbeat":
                            yield message("snapshot", state.view()) if card_refreshed else message("heartbeat", {})
                    if not resync:
                        yield message("stream_error", {"message": "Connection closed. Reloading a snapshot before reconnecting.", "retry_after": 2, "terminal": False})
                        return
            except APIError as error:
                yield message("stream_error", {"message": str(error), "retry_after": error.retry_after, "terminal": error.status in {401, 403, 404, 429}})
            except requests.RequestException:
                yield message("stream_error", {"message": "Connection interrupted. Reconnecting from a fresh snapshot.", "retry_after": 2, "terminal": False})
            finally:
                stream_slot.release()

        return Response(stream_with_context(generate()), content_type="text/event-stream", headers={"X-Accel-Buffering": "no"})

    return app


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Local next-to-jump racing board")
    parser.add_argument("--demo", action="store_true", help="Use explicitly fictional data; makes no upstream requests.")
    parser.add_argument("--port", type=int, default=5055)
    args = parser.parse_args()
    try:
        application = create_app(DemoAPI() if args.demo else None)
    except ValueError as error:
        parser.error(str(error))
    application.run(host="127.0.0.1", port=args.port, threaded=True, debug=False, use_reloader=False)
