"""Exemple de serveur de réception pour la « pycar Upload API v1 » (Python 3.10+, Flask).
Example receiving server for the pycar Upload API v1 — https://pycar.app/docs/api/

    pip install flask
    PYCAR_USER=demo PYCAR_PASSWORD=change-me python pycar_receiver.py
    → http://localhost:8080   (put it behind HTTPS in production: nginx, Caddy, a load balancer…)

In the pycar app: Settings › Photo upload (API)
    Server:   https://your-domain/pycar          (this script serves /oauth/token and /v1/…)
    Method:   OAuth 2.0 · login / password   →  PYCAR_USER / PYCAR_PASSWORD

Files are written to ./received/<vehicle_id>/ : vehicle.json, <photo_id>.jpg, <photo_id>.json, complete.json
"""
import hashlib, json, os, secrets, time
from functools import wraps
from flask import Flask, abort, jsonify, request

USER = os.environ.get("PYCAR_USER", "demo")
PASSWORD = os.environ.get("PYCAR_PASSWORD", "change-me")
ROOT = os.path.abspath(os.environ.get("PYCAR_DIR", "received"))
TOKEN_TTL = 3600

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 40 * 1024 * 1024        # 48 MP photos can exceed 15 MB
tokens, refresh_tokens = {}, {}                              # in memory: use a database in production


def error(code, err, desc):
    return jsonify(error=err, error_description=desc), code


@app.post("/oauth/token")
def token():
    """OAuth 2.0 token endpoint (RFC 6749): grant_type = password | client_credentials | refresh_token."""
    f = request.form
    grant = f.get("grant_type")
    if grant == "password":
        if not (secrets.compare_digest(f.get("username", ""), USER) and secrets.compare_digest(f.get("password", ""), PASSWORD)):
            return error(400, "invalid_grant", "Bad username or password")
    elif grant == "client_credentials":
        a = request.authorization                            # client_id / client_secret sent with HTTP Basic
        if not a or not (secrets.compare_digest(a.username or "", USER) and secrets.compare_digest(a.password or "", PASSWORD)):
            return error(401, "invalid_client", "Bad client credentials")
    elif grant == "refresh_token":
        if refresh_tokens.pop(f.get("refresh_token", ""), None) is None:
            return error(400, "invalid_grant", "Unknown refresh token")
    else:
        return error(400, "unsupported_grant_type", "Use password, client_credentials or refresh_token")
    access, refresh = secrets.token_urlsafe(32), secrets.token_urlsafe(32)
    tokens[access] = time.time() + TOKEN_TTL
    refresh_tokens[refresh] = True
    return jsonify(access_token=access, token_type="Bearer", expires_in=TOKEN_TTL, refresh_token=refresh, scope="pycar.upload")


def authenticated(fn):
    @wraps(fn)
    def wrapper(*a, **kw):
        h = request.headers.get("Authorization", "")
        if h.startswith("Bearer ") and tokens.get(h[7:], 0) > time.time():
            return fn(*a, **kw)
        b = request.authorization                            # HTTP Basic mode
        if b and b.type == "basic" and secrets.compare_digest(b.username or "", USER) and secrets.compare_digest(b.password or "", PASSWORD):
            return fn(*a, **kw)
        return error(401, "invalid_token", "Missing or expired credentials")
    return wrapper


def folder(vehicle_id):
    if not vehicle_id or "/" in vehicle_id or vehicle_id.startswith("."):
        abort(400)
    d = os.path.join(ROOT, vehicle_id)
    os.makedirs(d, exist_ok=True)
    return d


@app.get("/v1/ping")
@authenticated
def ping():
    return jsonify(ok=True, name="pycar receiver example", api="1")


@app.put("/v1/vehicles/<vehicle_id>")
@authenticated
def put_vehicle(vehicle_id):
    data = request.get_json(force=True)
    path = os.path.join(folder(vehicle_id), "vehicle.json")
    created = not os.path.exists(path)
    json.dump(data, open(path, "w"), indent=2, ensure_ascii=False)
    return jsonify(vehicle_id=vehicle_id, status="created" if created else "updated"), 201 if created else 200


@app.put("/v1/vehicles/<vehicle_id>/photos/<photo_id>")
@authenticated
def put_photo(vehicle_id, photo_id):
    meta = json.loads(request.form.get("metadata", "{}"))
    f = request.files.get("file")
    if f is None:
        return error(400, "invalid_request", "Missing file part")
    data = f.read()
    if meta.get("sha256") and hashlib.sha256(data).hexdigest() != meta["sha256"]:
        return error(422, "checksum_mismatch", "sha256 does not match the file")   # pycar shows the error; sending again retries
    d = folder(vehicle_id)
    path = os.path.join(d, photo_id + ".jpg")
    created = not os.path.exists(path)
    open(path, "wb").write(data)
    json.dump(meta, open(os.path.join(d, photo_id + ".json"), "w"), indent=2, ensure_ascii=False)
    return jsonify(photo_id=photo_id, status="created" if created else "updated"), 201 if created else 200


@app.post("/v1/vehicles/<vehicle_id>/complete")
@authenticated
def complete(vehicle_id):
    data = request.get_json(force=True)
    d = folder(vehicle_id)
    missing = [p for p in data.get("photo_ids", []) if not os.path.exists(os.path.join(d, p + ".jpg"))]
    json.dump(data, open(os.path.join(d, "complete.json"), "w"), indent=2)
    return jsonify(vehicle_id=vehicle_id, received=len(data.get("photo_ids", [])) - len(missing), missing=missing)


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
