import json
import os
import uuid
from datetime import datetime, timezone
from pathlib import Path
from threading import Lock

from flask import Flask, jsonify, render_template, request, send_from_directory


app = Flask(
    __name__,
    static_url_path="/staycation/static",
    static_folder="static",
)

BASE_PATH = "/staycation"
ACCESS_TOKEN = os.getenv("STAYCATION_ACCESS_TOKEN", "060874")
USERS = ("Ardy", "Marije", "Isis", "Ids")
CATEGORIES = ("Overdag", "Lunch", "Diner", "Avond")
FEASIBILITY_OPTIONS = ("Zeker haalbaar", "Nog even checken", "Uitzoeken", "Gereserveerd")
SCHEDULE_SLOTS = ("Ochtend", "Middag", "Eten", "Avond")
STAYCATION_DAYS = (
    {"date": "2026-07-18", "label": "Zaterdag 18 juli"},
    {"date": "2026-07-19", "label": "Zondag 19 juli"},
    {"date": "2026-07-20", "label": "Maandag 20 juli"},
    {"date": "2026-07-21", "label": "Dinsdag 21 juli"},
    {"date": "2026-07-22", "label": "Woensdag 22 juli"},
    {"date": "2026-07-23", "label": "Donderdag 23 juli"},
    {"date": "2026-07-24", "label": "Vrijdag 24 juli"},
    {"date": "2026-07-25", "label": "Zaterdag 25 juli"},
)
POINT_BUDGET = 20
MIN_IDEAS_PER_USER = 4
DATA_FILE = Path(
    os.getenv(
        "STAYCATION_DATA_FILE",
        Path(__file__).with_name("data") / "staycation_state.json",
    )
)
STATE_LOCK = Lock()
USER_ALIASES = {
    "Ardi": "Ardy",
    "Its": "Ids",
}
CATEGORY_ALIASES = {
    "Eten en lunch": "Lunch",
}


@app.route("/")
@app.route(BASE_PATH)
@app.route(f"{BASE_PATH}/")
def staycation_page():
    return render_template("tijdelijk_persoonlijk.html")


@app.route(f"{BASE_PATH}/health")
def health():
    return jsonify(
        {
            "app": "tijdelijk-persoonlijk",
            "status": "ok",
        }
    )


def empty_state():
    return {
        "ideas": [],
        "knownDevices": {},
        "notifications": {
            "lastSentDate": "",
            "pendingChanges": [],
        },
        "schedule": [],
        "vetoes": {},
        "votes": {user: {} for user in USERS},
    }


def read_state():
    if not DATA_FILE.exists():
        return empty_state()

    with DATA_FILE.open("r", encoding="utf-8") as handle:
        state = json.load(handle)

    state.setdefault("ideas", [])
    state.setdefault("knownDevices", {})
    state.setdefault("notifications", {})
    state["notifications"].setdefault("lastSentDate", "")
    state["notifications"].setdefault("pendingChanges", [])
    state.setdefault("schedule", [])
    state.pop("userClaims", None)
    state.setdefault("vetoes", {})
    state.setdefault("votes", {})
    state["votes"] = migrate_votes(state["votes"])
    state["vetoes"] = migrate_vetoes(state["vetoes"])
    for idea in state["ideas"]:
        idea["author"] = USER_ALIASES.get(idea.get("author"), idea.get("author"))
        idea["category"] = CATEGORY_ALIASES.get(idea.get("category"), idea.get("category"))
        idea.setdefault("preferredDate", "")
        idea.setdefault("preferredSlot", "")
    for user in USERS:
        state["votes"].setdefault(user, {})
    return state


def migrate_votes(votes):
    migrated = {}
    for user, user_votes in votes.items():
        new_user = USER_ALIASES.get(user, user)
        if new_user not in migrated:
            migrated[new_user] = {}
        migrated[new_user].update(user_votes)
    return migrated


def migrate_vetoes(vetoes):
    migrated = {}
    for user, idea_id in vetoes.items():
        migrated[USER_ALIASES.get(user, user)] = idea_id
    return migrated


def write_state(state):
    DATA_FILE.parent.mkdir(parents=True, exist_ok=True)
    with DATA_FILE.open("w", encoding="utf-8") as handle:
        json.dump(state, handle, ensure_ascii=True, indent=2)


def clean_text(value, limit):
    return str(value or "").strip()[:limit]


def clean_device_id(value):
    return "".join(char for char in str(value or "") if char.isalnum() or char == "-")[:80]


def device_is_known(state, device_id):
    return bool(device_id and device_id in state.get("knownDevices", {}))


def require_known_device(state, device_id):
    if not device_is_known(state, device_id):
        return jsonify({"error": "Voer eerst de toegangscode in op dit toestel."}), 403
    return None


def record_change(state, actor, text, kind="general", idea_name=""):
    if not text:
        return
    notifications = state.setdefault("notifications", {})
    pending = notifications.setdefault("pendingChanges", [])
    pending.append(
        {
            "actor": actor if actor in USERS else "",
            "ideaName": clean_text(idea_name, 100),
            "kind": clean_text(kind, 30),
            "text": clean_text(text, 180),
            "createdAt": datetime.now(timezone.utc).isoformat(),
        }
    )
    del pending[:-50]


def idea_counts(state):
    counts = {user: 0 for user in USERS}
    for idea in state["ideas"]:
        author = idea.get("author")
        if author in counts:
            counts[author] += 1
    return counts


def voting_open(state):
    counts = idea_counts(state)
    return all(count >= MIN_IDEAS_PER_USER for count in counts.values())


def validate_idea_timing(date, slot):
    if date and date not in {day["date"] for day in STAYCATION_DAYS}:
        return "Kies een dag binnen de staycation."
    if slot and slot not in SCHEDULE_SLOTS:
        return "Kies een geldig dagdeel."
    if slot and not date:
        return "Kies ook een datum bij dit moment."
    return ""


def public_state(state):
    counts = idea_counts(state)
    totals = {}
    for votes in state["votes"].values():
        for idea_id, points in votes.items():
            totals[idea_id] = totals.get(idea_id, 0) + int(points)
    veto_totals = {}
    for idea_id in state["vetoes"].values():
        veto_totals[idea_id] = veto_totals.get(idea_id, 0) + 1

    return {
        "users": list(USERS),
        "categories": list(CATEGORIES),
        "feasibilityOptions": list(FEASIBILITY_OPTIONS),
        "scheduleSlots": list(SCHEDULE_SLOTS),
        "staycationDays": list(STAYCATION_DAYS),
        "pointBudget": POINT_BUDGET,
        "minIdeasPerUser": MIN_IDEAS_PER_USER,
        "votingOpen": voting_open(state),
        "ideaCounts": counts,
        "ideas": state["ideas"],
        "schedule": state["schedule"],
        "vetoTotals": veto_totals,
        "vetoes": state["vetoes"],
        "votes": state["votes"],
        "totals": totals,
    }


@app.route(f"{BASE_PATH}/api/state")
def api_state():
    with STATE_LOCK:
        state = read_state()
    return jsonify(public_state(state))


@app.route(f"{BASE_PATH}/api/access", methods=["POST"])
def api_grant_access():
    data = request.get_json(silent=True) or {}
    device_id = clean_device_id(data.get("deviceId"))
    token = clean_text(data.get("token"), 40)

    if not device_id:
        return jsonify({"error": "Dit toestel heeft geen geldige code."}), 400
    if token != ACCESS_TOKEN:
        return jsonify({"error": "De toegangscode klopt niet."}), 403

    with STATE_LOCK:
        state = read_state()
        known_devices = state.setdefault("knownDevices", {})
        if device_id not in known_devices:
            known_devices[device_id] = {
                "grantedAt": datetime.now(timezone.utc).isoformat(),
            }
            record_change(state, "", "Een toestel heeft toegang gekregen.", "access")
            write_state(state)
        response = public_state(state)

    return jsonify(response)


@app.route(f"{BASE_PATH}/api/ideas", methods=["POST"])
def api_add_idea():
    data = request.get_json(silent=True) or {}
    author = clean_text(data.get("author"), 30)
    device_id = clean_device_id(data.get("deviceId"))
    category = clean_text(data.get("category"), 40)
    name = clean_text(data.get("name"), 80)
    description = clean_text(data.get("description"), 500)
    duration = clean_text(data.get("duration"), 40)
    location = clean_text(data.get("location"), 80)
    feasibility = clean_text(data.get("feasibility"), 40) or FEASIBILITY_OPTIONS[0]
    preferred_date = clean_text(data.get("preferredDate"), 20)
    preferred_slot = clean_text(data.get("preferredSlot"), 30)

    if author not in USERS:
        return jsonify({"error": "Kies eerst een geldige gebruiker."}), 400
    if category not in CATEGORIES:
        return jsonify({"error": "Kies een geldige groep."}), 400
    if not name:
        return jsonify({"error": "Geef het idee een naam."}), 400
    if feasibility not in FEASIBILITY_OPTIONS:
        return jsonify({"error": "Kies een geldige haalbaarheid."}), 400
    timing_error = validate_idea_timing(preferred_date, preferred_slot)
    if timing_error:
        return jsonify({"error": timing_error}), 400

    idea = {
        "id": uuid.uuid4().hex,
        "author": author,
        "category": category,
        "name": name,
        "description": description,
        "duration": duration,
        "feasibility": feasibility,
        "location": location,
        "preferredDate": preferred_date,
        "preferredSlot": preferred_slot,
        "createdAt": datetime.now(timezone.utc).isoformat(),
    }

    with STATE_LOCK:
        state = read_state()
        access_error = require_known_device(state, device_id)
        if access_error:
            return access_error
        state["ideas"].insert(0, idea)
        record_change(state, author, f"{author} heeft een idee toegevoegd: {name}.", "idea_added", name)
        write_state(state)
        response = public_state(state)

    return jsonify(response), 201


@app.route(f"{BASE_PATH}/api/ideas/<idea_id>", methods=["PATCH"])
def api_update_idea(idea_id):
    data = request.get_json(silent=True) or {}
    user = clean_text(data.get("user"), 30)
    device_id = clean_device_id(data.get("deviceId"))
    category = clean_text(data.get("category"), 40)
    name = clean_text(data.get("name"), 80)
    description = clean_text(data.get("description"), 500)
    duration = clean_text(data.get("duration"), 40)
    location = clean_text(data.get("location"), 80)
    feasibility = clean_text(data.get("feasibility"), 40) or FEASIBILITY_OPTIONS[0]
    preferred_date = clean_text(data.get("preferredDate"), 20)
    preferred_slot = clean_text(data.get("preferredSlot"), 30)

    if user not in USERS:
        return jsonify({"error": "Kies eerst een geldige gebruiker."}), 400
    if category not in CATEGORIES:
        return jsonify({"error": "Kies een geldige groep."}), 400
    if not name:
        return jsonify({"error": "Geef het idee een naam."}), 400
    if feasibility not in FEASIBILITY_OPTIONS:
        return jsonify({"error": "Kies een geldige haalbaarheid."}), 400
    timing_error = validate_idea_timing(preferred_date, preferred_slot)
    if timing_error:
        return jsonify({"error": timing_error}), 400

    with STATE_LOCK:
        state = read_state()
        access_error = require_known_device(state, device_id)
        if access_error:
            return access_error
        idea = next((item for item in state["ideas"] if item["id"] == idea_id), None)
        if not idea:
            return jsonify({"error": "Dit idee bestaat niet meer."}), 404
        if idea.get("author") != user:
            return jsonify({"error": "Je kunt alleen je eigen ideeen wijzigen."}), 403

        idea.update(
            {
                "category": category,
                "name": name,
                "description": description,
                "duration": duration,
                "feasibility": feasibility,
                "location": location,
                "preferredDate": preferred_date,
                "preferredSlot": preferred_slot,
                "updatedAt": datetime.now(timezone.utc).isoformat(),
            }
        )
        record_change(state, user, f"{user} heeft een idee gewijzigd: {name}.", "idea_updated", name)
        write_state(state)
        response = public_state(state)

    return jsonify(response)


@app.route(f"{BASE_PATH}/api/votes", methods=["POST"])
def api_set_vote():
    data = request.get_json(silent=True) or {}
    user = clean_text(data.get("user"), 30)
    device_id = clean_device_id(data.get("deviceId"))
    idea_id = clean_text(data.get("ideaId"), 80)

    try:
        points = int(data.get("points", 0))
    except (TypeError, ValueError):
        return jsonify({"error": "Gebruik een geldig aantal punten."}), 400

    if user not in USERS:
        return jsonify({"error": "Kies eerst een geldige gebruiker."}), 400
    if points < 0:
        return jsonify({"error": "Punten kunnen niet negatief zijn."}), 400

    with STATE_LOCK:
        state = read_state()
        access_error = require_known_device(state, device_id)
        if access_error:
            return access_error
        if idea_id not in {idea["id"] for idea in state["ideas"]}:
            return jsonify({"error": "Dit idee bestaat niet meer."}), 404
        if not voting_open(state) and points > 0:
            return jsonify({"error": "Stemmen kan pas als iedereen vier ideeen heeft toegevoegd."}), 403
        user_votes = state["votes"].setdefault(user, {})
        other_total = sum(
            int(value)
            for key, value in user_votes.items()
            if key != idea_id
        )
        if other_total + points > POINT_BUDGET:
            return jsonify({"error": "Je hebt niet genoeg punten over."}), 400

        if points == 0:
            user_votes.pop(idea_id, None)
        else:
            user_votes[idea_id] = points
        idea = next((item for item in state["ideas"] if item["id"] == idea_id), None)
        if idea:
            record_change(state, user, f"{user} heeft gestemd.", "vote_changed")
        write_state(state)
        response = public_state(state)

    return jsonify(response)


@app.route(f"{BASE_PATH}/api/vetoes", methods=["POST"])
def api_set_veto():
    data = request.get_json(silent=True) or {}
    user = clean_text(data.get("user"), 30)
    device_id = clean_device_id(data.get("deviceId"))
    idea_id = clean_text(data.get("ideaId"), 80)
    action = clean_text(data.get("action"), 20) or "set"

    if user not in USERS:
        return jsonify({"error": "Kies eerst een geldige gebruiker."}), 400

    with STATE_LOCK:
        state = read_state()
        access_error = require_known_device(state, device_id)
        if access_error:
            return access_error
        if action == "clear":
            old_idea = next(
                (idea for idea in state["ideas"] if idea["id"] == state["vetoes"].get(user)),
                None,
            )
            state["vetoes"].pop(user, None)
            if old_idea:
                record_change(
                    state,
                    user,
                    f"{user} heeft het veto weggehaald bij: {old_idea['name']}.",
                    "veto_cleared",
                    old_idea["name"],
                )
        else:
            idea = next((item for item in state["ideas"] if item["id"] == idea_id), None)
            if not idea:
                return jsonify({"error": "Dit idee bestaat niet meer."}), 404
            state["vetoes"][user] = idea_id
            record_change(state, user, f"{user} heeft een veto gezet op: {idea['name']}.", "veto_set", idea["name"])
        write_state(state)
        response = public_state(state)

    return jsonify(response)


@app.route(f"{BASE_PATH}/api/schedule", methods=["POST"])
def api_schedule_idea():
    data = request.get_json(silent=True) or {}
    user = clean_text(data.get("user"), 30)
    device_id = clean_device_id(data.get("deviceId"))
    idea_id = clean_text(data.get("ideaId"), 80)
    date = clean_text(data.get("date"), 20)
    slot = clean_text(data.get("slot"), 30)
    action = clean_text(data.get("action"), 20) or "add"

    if user != "Ardy":
        return jsonify({"error": "Alleen Ardy kan ideeen in het schema plaatsen."}), 403
    if date not in {day["date"] for day in STAYCATION_DAYS}:
        return jsonify({"error": "Kies een dag binnen de staycation."}), 400
    if slot not in SCHEDULE_SLOTS:
        return jsonify({"error": "Kies een geldig dagdeel."}), 400

    with STATE_LOCK:
        state = read_state()
        access_error = require_known_device(state, device_id)
        if access_error:
            return access_error
        if idea_id not in {idea["id"] for idea in state["ideas"]}:
            return jsonify({"error": "Dit idee bestaat niet meer."}), 404

        state["schedule"] = [
            item
            for item in state["schedule"]
            if not (
                item.get("ideaId") == idea_id
                and item.get("date") == date
                and item.get("slot") == slot
            )
        ]
        if action != "remove":
            state["schedule"].append(
                {
                    "id": uuid.uuid4().hex,
                    "ideaId": idea_id,
                    "date": date,
                    "slot": slot,
                    "createdAt": datetime.now(timezone.utc).isoformat(),
                }
            )
        idea = next((item for item in state["ideas"] if item["id"] == idea_id), None)
        if idea:
            verb = "uit het weekoverzicht gehaald" if action == "remove" else "in het weekoverzicht gezet"
            record_change(state, user, f"{user} heeft {idea['name']} {verb} ({date}, {slot}).", "schedule_changed", idea["name"])
        write_state(state)
        response = public_state(state)

    return jsonify(response)


@app.route("/favicon.ico")
def favicon():
    return send_from_directory(
        "static/img",
        "staycation-neon.png",
        mimetype="image/png",
    )


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=6005, debug=True)
