"""IP lookup API. Service probes deliberately run only in the local checker."""
from collections import OrderedDict, deque
from contextlib import asynccontextmanager
import asyncio
import hashlib
import ipaddress
import os
import secrets
import threading
import time

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

from .diagnostics import ip_report, public_ip, PROVIDERS, VERSION

TRUST_NGINX = os.environ.get("VISHOW_TRUST_LOCAL_PROXY", "0") == "1"
cache = OrderedDict()
requests_by_client = OrderedDict()
global_requests = deque()
lock = threading.Lock()
slots = threading.BoundedSemaphore(2)
pepper = secrets.token_bytes(32)
CACHE_SECONDS = 300


@asynccontextmanager
async def lifespan(application):
    async def expire():
        while True:
            await asyncio.sleep(5)
            now = time.monotonic()
            with lock:
                for key in list(cache):
                    if cache[key][0] <= now:
                        del cache[key]
                for key in list(requests_by_client):
                    if not requests_by_client[key] or requests_by_client[key][-1] < now-60:
                        del requests_by_client[key]
    task = asyncio.create_task(expire())
    try:
        yield
    finally:
        task.cancel()
        try:
            await task
        except asyncio.CancelledError:
            pass
        with lock:
            cache.clear()
            requests_by_client.clear()
            global_requests.clear()


app = FastAPI(title="Vishow IP API",docs_url=None,redoc_url=None,openapi_url=None,lifespan=lifespan)


def client_ip(request):
    peer = request.client.host if request.client else ""
    if TRUST_NGINX:
        try:
            trusted = request.client is None or ipaddress.ip_address(peer).is_loopback
        except ValueError:
            trusted = False
        if trusted:
            candidate = request.headers.get("x-real-ip", "")
            try:
                return str(ipaddress.ip_address(candidate))
            except ValueError:
                raise HTTPException(400,"Invalid proxy address.") from None
    return peer


def limit_client(ip):
    now = time.monotonic()
    digest = hashlib.blake2b(ip.encode(),key=pepper,digest_size=16).digest()
    with lock:
        times = requests_by_client.setdefault(digest,deque())
        while times and times[0] < now-60:
            times.popleft()
        while global_requests and global_requests[0] < now-60:
            global_requests.popleft()
        if len(times) >= 6 or len(global_requests) >= 30:
            raise HTTPException(429,"Please wait before checking again.",headers={"Retry-After":"60"})
        times.append(now)
        global_requests.append(now)
        requests_by_client.move_to_end(digest)
        while len(requests_by_client) > 2048:
            requests_by_client.popitem(last=False)
        for key in list(cache):
            if cache[key][0] <= now:
                del cache[key]


@app.middleware("http")
async def response_policy(request,call_next):
    origin = request.headers.get("origin")
    allowed = os.environ.get("VISHOW_ORIGIN","https://vishow.top")
    if request.method == "POST" and origin and origin != allowed:
        return JSONResponse({"detail":"Origin is not allowed."},status_code=403)
    response = await call_next(request)
    response.headers["Cache-Control"] = "no-store"
    response.headers["X-Content-Type-Options"] = "nosniff"
    return response


@app.get("/api/health")
def health():
    return {"ok":True,"version":VERSION}


@app.get("/api/context")
def context(request: Request):
    ip = client_ip(request)
    return {"ip":ip,"basic_enabled":bool(os.environ.get("IPINFO_TOKEN")),
            "providers_enabled":[name for _,(name,key) in PROVIDERS.items() if os.environ.get(key)],
            "cache_seconds":CACHE_SECONDS}


@app.get("/all.json")
def all_headers(request: Request):
    # JSONResponse performs escaping; never interpolate headers into JSON text.
    return {"ip":client_ip(request),"host":request.headers.get("host",""),
            "ua":request.headers.get("user-agent",""),
            "port":request.headers.get("x-real-port","") if TRUST_NGINX and (request.client is None or request.client.host in ("127.0.0.1","::1")) else str(request.client.port if request.client else ""),
            "lang":request.headers.get("accept-language",""),"encoding":request.headers.get("accept-encoding",""),
            "mime":request.headers.get("accept",""),"referer":request.headers.get("referer",""),
            "forwarded":request.headers.get("x-forwarded-for",""),"method":request.method}


class Lookup(BaseModel):
    ip: str | None = Field(default=None,max_length=45)


@app.post("/api/ip-report")
def report(body: Lookup, request: Request):
    if not request.headers.get("content-type","").lower().startswith("application/json"):
        raise HTTPException(415,"Use application/json.")
    viewer = client_ip(request)
    limit_client(viewer)
    try:
        target = public_ip(body.ip or viewer)
    except ValueError as error:
        raise HTTPException(422,str(error)) from None
    with lock:
        cached = cache.get(target)
        if cached and cached[0] > time.monotonic():
            return dict(cached[1],cached=True)
    if not slots.acquire(blocking=False):
        raise HTTPException(503,"A check is already in progress. Please retry shortly.",headers={"Retry-After":"15"})
    try:
        result = ip_report(target)
        with lock:
            cache[target] = (time.monotonic()+CACHE_SECONDS,result)
            cache.move_to_end(target)
            while len(cache)>128:
                cache.popitem(last=False)
        return dict(result,cached=False)
    finally:
        slots.release()
