# -*- coding: utf-8 -*-
"""Le chat Mistou 2 – interactive French for Grade 2.

Roles: admin (platform) -> parent (tenant, owns up to max_kids kids) -> kid (sub-tenant, the learner)."""
import os
from datetime import timedelta
from functools import wraps
from dotenv import load_dotenv
from flask import Flask, request, jsonify, session, render_template, redirect, send_from_directory, abort, g

load_dotenv()

import db
import gamify
import accounts as ACC
import curriculum as CUR
from curriculum import en as EN
from curriculum.en_lexicon import LEX

app = Flask(__name__)
app.secret_key = os.environ.get("SECRET_KEY", "french-gr2-dev-secret")


def _asset_version():
    """Changes whenever a script or stylesheet changes, so phones never run an old cached copy after a deploy."""
    import hashlib
    h = hashlib.md5()
    for sub in ("js", "css"):
        d = os.path.join(app.static_folder, sub)
        for f in sorted(os.listdir(d)):
            h.update(f.encode()); h.update(str(os.path.getmtime(os.path.join(d, f))).encode())
    return h.hexdigest()[:8]


@app.context_processor
def _inject_version():
    return dict(v=_asset_version() if app.debug else ASSET_V)


ASSET_V = _asset_version()
app.config.update(JSON_AS_ASCII=False, SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE="Lax", PERMANENT_SESSION_LIFETIME=timedelta(days=30))
VOCAB_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static", "vocab")
HOME = {"kid": "/", "parent": "/parent", "admin": "/admin"}


def build_en():
    """French -> English for the curriculum plus levels, badges and unit descriptions."""
    m = EN.build(CUR.UNITS)
    extra = [l[1] for l in gamify.LEVELS] + [b["fr"] for b in gamify.BADGES] + [b["desc_fr"] for b in gamify.BADGES] + [u["desc_fr"] for u in CUR.UNITS] + [l["desc_fr"] for l in CUR.LEVELS] + [l["title_fr"] for l in CUR.LEVELS]
    for s in extra:
        t = EN.lookup(s)
        if t:
            m[EN.norm(s)] = t
    return m


EN_MAP = build_en()

db.init_db()
db.migrate_timezone()
ACC.migrate()
ACC.bootstrap(os.environ)


# ---------------------------------------------------------------- auth helpers
def current_user():
    """The logged-in account, reloaded on every request so a suspension or deletion takes effect at once."""
    if "user" not in g:
        u = ACC.get(session.get("uid")) if session.get("uid") else None
        g.user = u if u and ACC.can_login(u) else None
    return g.user


def login_required(*roles):
    def deco(f):
        @wraps(f)
        def wrapper(*a, **kw):
            u = current_user()
            api = request.path.startswith("/api/")
            if not u:
                session.clear()
                return (jsonify(error="not_logged_in"), 401) if api else redirect("/login")
            if roles and u["role"] not in roles:
                return (jsonify(error="wrong_role"), 403) if api else redirect(HOME[u["role"]])
            return f(*a, **kw)
        return wrapper
    return deco


def fail(msg, code=400):
    return jsonify(error=msg), code


def body():
    return request.get_json(force=True, silent=True) or {}


def kid_band(kid):
    lv = CUR.level(kid.get("course_level") or 1)
    return lv["band"] if lv else "kids"


def managed_kid(kid_id):
    """The kid the caller may manage (admin: any kid; parent: only its own), else None."""
    try:
        k = ACC.get(int(kid_id))
    except (TypeError, ValueError):
        return None
    return k if ACC.can_manage_kid(current_user(), k) else None


# ---------------------------------------------------------------- pages
@app.route("/login")
def login_page():
    u = current_user()
    if u:
        return redirect(HOME[u["role"]])
    return render_template("login.html")


@app.route("/logout")
def logout():
    session.clear()
    return redirect("/login")


@app.route("/")
@login_required("kid")
def kid_page():
    u = current_user()
    return render_template("kid.html", kid=dict(name=u["display_name"], id=u["id"], avatar=u["avatar"]))


@app.route("/parent")
@login_required("parent", "admin")
def parent_page():
    return render_template("parent.html", me=ACC.public(current_user()))


@app.route("/admin")
@login_required("admin")
def admin_page():
    return render_template("admin.html", me=ACC.public(current_user()))


@app.route("/vocab/<key>.png")
def vocab_img(key):
    if not key.replace("_", "").isalnum():
        abort(404)
    return send_from_directory(VOCAB_DIR, key + ".png", max_age=86400)


@app.post("/api/login")
def api_login():
    d = body()
    u, err = ACC.authenticate(d.get("username", ""), d.get("password", ""))
    if err:
        return fail(err, 200)
    session.clear()
    session.update(uid=u["id"], role=u["role"])
    session.permanent = True
    return jsonify(ok=True, role=u["role"], home=HOME[u["role"]])


@app.post("/api/account/password")
@login_required("parent", "admin")
def api_own_password():
    d = body()
    u = current_user()
    if not ACC.check_pw(u, d.get("old", "")):
        return fail("كلمة المرور الحالية غير صحيحة")
    try:
        ACC.set_password(u["id"], d.get("new", ""))
    except ACC.AccountError as e:
        return fail(str(e))
    return jsonify(ok=True)


# ---------------------------------------------------------------- kid API
@app.get("/api/curriculum")
@login_required()
def api_curriculum():
    u = current_user()
    lvl = u["course_level"] if u["role"] == "kid" else 1
    band = CUR.level(lvl)["band"]
    return jsonify(units=CUR.units_of(lvl), course=CUR.level(lvl), band=band, levels=gamify.RANKS[band], badges=gamify.badges_for(lvl), en=EN_MAP, lex=LEX, avatars=ACC.AVATARS_BY_BAND.get(band, ACC.AVATARS),
                   imgs=sorted(f[:-4] for f in os.listdir(VOCAB_DIR) if f.endswith(".png")) if os.path.isdir(VOCAB_DIR) else [])


@app.get("/api/me")
@login_required("kid")
def api_me():
    u = current_user()
    st = db.stats(u["id"])
    return jsonify(progress=db.progress_map(u["id"]), stats=st, level=gamify.level_of(st["xp"], kid_band(u)), unlock_all=bool(u["unlock_all"]),
                   name=u["display_name"], avatar=u["avatar"])


@app.post("/api/me/avatar")
@login_required("kid")
def api_my_avatar():
    try:
        ACC.update(current_user()["id"], avatar=body().get("avatar"))
    except ACC.AccountError as e:
        return fail(str(e))
    return jsonify(ok=True)


@app.post("/api/complete")
@login_required("kid")
def api_complete():
    d = body()
    aid = d.get("activity_id")
    if aid not in CUR.ACTIVITIES:
        return fail("unknown activity")
    if CUR.LEVEL_OF_ACTIVITY[aid] != current_user()["course_level"]:
        return fail("this activity is not in your level")
    lid, act = CUR.ACTIVITIES[aid]
    u = current_user()
    uid = u["id"]
    res = db.record_attempt(uid, lid, aid, d.get("score", 0), d.get("total", 0), act["points"], d.get("seconds", 0), partial=bool(d.get("partial")))
    st = db.stats(uid)
    progress = db.progress_map(uid)
    new_badges = []
    for b in gamify.check_badges(st, progress, CUR.LESSONS, CUR.units_of(u["course_level"]), u["course_level"]):
        if db.award_badge(uid, b):
            new_badges.append(next(x for x in gamify.BADGES if x["id"] == b))
    st = db.stats(uid)
    lesson_done = gamify.lesson_done(progress, CUR.LESSONS[lid])
    return jsonify(result=res, stats=st, level=gamify.level_of(st["xp"], kid_band(u)), new_badges=new_badges, progress=progress, lesson_done=lesson_done)


# ---------------------------------------------------------------- one kid's progress (parent of the kid, or admin)
@app.get("/api/kid/<int:kid_id>/overview")
@login_required("parent", "admin")
def api_kid_overview(kid_id):
    k = managed_kid(kid_id)
    if not k:
        return fail("غير مسموح", 403)
    st = db.stats(k["id"])
    return jsonify(kid=ACC.public(k), stats=st, level=gamify.level_of(st["xp"], kid_band(k)), progress=db.progress_map(k["id"]),
                   outline=CUR.outline(k["course_level"]), course=CUR.level(k["course_level"]), badges=gamify.badges_for(k["course_level"]), days=db.day_series(k["id"], 14))


@app.get("/api/kid/<int:kid_id>/history")
@login_required("parent", "admin")
def api_kid_history(kid_id):
    k = managed_kid(kid_id)
    if not k:
        return fail("غير مسموح", 403)
    titles = {aid: dict(title=a["title"], lesson=CUR.LESSONS[lid]["title"], lesson_ar=CUR.LESSONS[lid]["title_ar"]) for aid, (lid, a) in CUR.ACTIVITIES.items()}
    return jsonify(history=db.history(k["id"], 500), titles=titles)


@app.post("/api/kid/<int:kid_id>/reset")
@login_required("parent", "admin")
def api_kid_reset(kid_id):
    k = managed_kid(kid_id)
    if not k:
        return fail("غير مسموح", 403)
    d = body()
    db.reset(k["id"], d.get("lesson_id"), d.get("activity_id"))
    return jsonify(ok=True)


# ---------------------------------------------------------------- kids management (a parent: its own kids; admin: any)
def _parent_scope():
    """The parent whose kids the request is about: a parent itself, or the admin's ?parent= / body parent_id."""
    u = current_user()
    if u["role"] == "parent":
        return u
    pid = request.args.get("parent") or body().get("parent_id")
    p = ACC.get(int(pid)) if pid and str(pid).isdigit() else None
    return p if p and p["role"] == "parent" else None


@app.get("/api/kids")
@login_required("parent", "admin")
def api_kids():
    u = current_user()
    if u["role"] == "admin" and not request.args.get("parent"):
        return jsonify(kids=ACC.all_kids(), parents={p["id"]: p for p in ACC.parents()}, avatars=ACC.AVATARS, levels=CUR.LEVELS)
    p = _parent_scope()
    if not p:
        return fail("ولي الأمر غير موجود", 404)
    return jsonify(kids=ACC.kids_of(p["id"]), parent=ACC.public(p), max_kids=p["max_kids"], avatars=ACC.AVATARS, levels=CUR.LEVELS)


@app.post("/api/kids")
@login_required("parent", "admin")
def api_kid_create():
    p = _parent_scope()
    if not p:
        return fail("اختر ولي الأمر")
    d = body()
    password = d.get("password") or ACC.gen_password()
    try:
        k = ACC.create("kid", d.get("username"), d.get("display_name"), password, parent_id=p["id"], avatar=d.get("avatar", ""),
                       course_level=d.get("course_level", 1))
    except ACC.AccountError as e:
        return fail(str(e))
    return jsonify(ok=True, kid=ACC.public(k), password=password)


@app.post("/api/kids/<int:kid_id>")
@login_required("parent", "admin")
def api_kid_update(kid_id):
    k = managed_kid(kid_id)
    if not k:
        return fail("غير مسموح", 403)
    d = body()
    try:
        k = ACC.update(k["id"], **{f: d[f] for f in ("display_name", "avatar", "unlock_all", "active", "course_level") if f in d})
    except ACC.AccountError as e:
        return fail(str(e))
    return jsonify(ok=True, kid=ACC.public(k))


@app.post("/api/kids/<int:kid_id>/password")
@login_required("parent", "admin")
def api_kid_password(kid_id):
    k = managed_kid(kid_id)
    if not k:
        return fail("غير مسموح", 403)
    password = body().get("password") or ACC.gen_password()
    try:
        ACC.set_password(k["id"], password)
    except ACC.AccountError as e:
        return fail(str(e))
    return jsonify(ok=True, password=password, username=k["username"])


@app.post("/api/kids/<int:kid_id>/delete")
@login_required("parent", "admin")
def api_kid_delete(kid_id):
    k = managed_kid(kid_id)
    if not k:
        return fail("غير مسموح", 403)
    ACC.delete(k["id"])
    return jsonify(ok=True)


# ---------------------------------------------------------------- parents management (admin)
def _parent(pid):
    p = ACC.get(pid)
    return p if p and p["role"] == "parent" else None


@app.get("/api/admin/summary")
@login_required("admin")
def api_admin_summary():
    return jsonify(ACC.summary())


@app.get("/api/admin/parents")
@login_required("admin")
def api_admin_parents():
    return jsonify(parents=ACC.parents())


@app.post("/api/admin/parents")
@login_required("admin")
def api_admin_parent_create():
    d = body()
    password = d.get("password") or ACC.gen_password()
    try:
        p = ACC.create("parent", d.get("username"), d.get("display_name"), password, max_kids=d.get("max_kids", ACC.DEFAULT_MAX_KIDS))
    except ACC.AccountError as e:
        return fail(str(e))
    return jsonify(ok=True, parent=ACC.public(p), password=password)


@app.post("/api/admin/parents/<int:pid>")
@login_required("admin")
def api_admin_parent_update(pid):
    if not _parent(pid):
        return fail("ولي الأمر غير موجود", 404)
    d = body()
    try:
        p = ACC.update(pid, **{f: d[f] for f in ("display_name", "max_kids", "active") if f in d})
    except ACC.AccountError as e:
        return fail(str(e))
    return jsonify(ok=True, parent=ACC.public(p))


@app.post("/api/admin/parents/<int:pid>/password")
@login_required("admin")
def api_admin_parent_password(pid):
    p = _parent(pid)
    if not p:
        return fail("ولي الأمر غير موجود", 404)
    password = body().get("password") or ACC.gen_password()
    try:
        ACC.set_password(pid, password)
    except ACC.AccountError as e:
        return fail(str(e))
    return jsonify(ok=True, password=password, username=p["username"])


@app.post("/api/admin/parents/<int:pid>/delete")
@login_required("admin")
def api_admin_parent_delete(pid):
    if not _parent(pid):
        return fail("ولي الأمر غير موجود", 404)
    n = ACC.delete(pid)
    return jsonify(ok=True, deleted=n)


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5060, debug=True)
