"""Notifications: persisted in-app inbox + Web Push (PWA).

Delivery rules (access-controlled):
  - "assigned"  -> only the task's assignee is notified.
  - "completed" -> only the assignee's team lead is notified, and only when a
                   subordinate completes it. A lead completing their own task
                   notifies no one.
Self-actions never notify the actor.

VAPID keys are read from config (env) if provided, otherwise generated once and
persisted to storage/vapid.json so setup needs no manual steps.
"""
import base64
import json
import logging
import os

from flask import (
    Blueprint,
    current_app,
    jsonify,
    render_template,
    request,
    url_for,
)
from flask_login import current_user, login_required

from . import storage

notifications_bp = Blueprint("notifications", __name__)
log = logging.getLogger(__name__)


# --------------------------------------------------------------------------
# VAPID key management
# --------------------------------------------------------------------------
def init_notifications(app):
    pub = (app.config.get("VAPID_PUBLIC_KEY") or "").strip()
    priv = (app.config.get("VAPID_PRIVATE_KEY") or "").strip()
    if not (pub and priv):
        pub, priv = _load_or_generate_keys(app)
    app.config["VAPID_PUBLIC_KEY"] = pub
    app.config["VAPID_PRIVATE_KEY"] = priv
    sub = (app.config.get("VAPID_CLAIM_EMAIL") or "").strip() or "admin@onit.local"
    app.config["VAPID_CLAIM_EMAIL"] = sub if sub.startswith("mailto:") else "mailto:" + sub


def _vapid_path(app):
    return os.path.join(app.config["STORAGE_DIR"], "vapid.json")


def _load_or_generate_keys(app):
    path = _vapid_path(app)
    if os.path.exists(path):
        try:
            with open(path, "r", encoding="utf-8") as fh:
                data = json.load(fh)
            if data.get("public_key") and data.get("private_key"):
                return data["public_key"], data["private_key"]
        except (OSError, ValueError):
            pass

    from cryptography.hazmat.primitives import serialization
    from py_vapid import Vapid02

    v = Vapid02()
    v.generate_keys()
    # pywebpush expects the private key as a base64url-encoded raw 32-byte value.
    raw_priv = v.private_key.private_numbers().private_value.to_bytes(32, "big")
    priv_b64 = base64.urlsafe_b64encode(raw_priv).rstrip(b"=").decode()
    raw_pub = v.public_key.public_bytes(
        serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint
    )
    pub_b64 = base64.urlsafe_b64encode(raw_pub).rstrip(b"=").decode()
    try:
        with open(path, "w", encoding="utf-8") as fh:
            json.dump({"public_key": pub_b64, "private_key": priv_b64}, fh)
    except OSError:
        log.warning("Could not persist VAPID keys to %s", path)
    return pub_b64, priv_b64


# --------------------------------------------------------------------------
# Sending
# --------------------------------------------------------------------------
def _send(subscription, payload):
    from pywebpush import WebPushException, webpush

    try:
        webpush(
            subscription_info=subscription,
            data=json.dumps(payload),
            vapid_private_key=current_app.config["VAPID_PRIVATE_KEY"],
            vapid_claims={"sub": current_app.config["VAPID_CLAIM_EMAIL"]},
            timeout=10,
        )
        return True
    except WebPushException as exc:
        status = getattr(exc.response, "status_code", None)
        if status in (404, 410):  # gone / not found -> drop the stale subscription
            storage.remove_subscription(subscription.get("endpoint"))
        else:
            log.warning("Web push failed (%s): %s", status, exc)
        return False
    except Exception as exc:  # never let a push error break the request
        log.warning("Web push error: %s", exc)
        return False


def push_to_user(user_id, payload):
    for record in storage.get_subscriptions(user_id):
        _send(record["subscription"], payload)


def notify(user_id, ntype, title, body, task_id=None, actor_id=None, url=None):
    """Persist an in-app notification and push it. No-op for self-actions."""
    if user_id is None or user_id == actor_id:
        return
    storage.add_notification(user_id, ntype, title, body, task_id)
    payload = {"title": title, "body": body, "url": url or url_for("notifications.inbox")}
    push_to_user(user_id, payload)


# --------------------------------------------------------------------------
# Triggers (called from task views)
# --------------------------------------------------------------------------
def task_assigned(task, actor_id=None):
    notify(
        task["assignee_id"],
        "assigned",
        "New task assigned",
        task["title"],
        task_id=task["id"],
        actor_id=actor_id,
        url=url_for("tasks.detail", task_id=task["id"]),
    )


def task_completed(task, actor_id=None):
    assignee = storage.get_user(task["assignee_id"])
    if assignee is None:
        return
    # Only a subordinate's completion notifies the lead; a lead's own task does not.
    if assignee.get("role") != "subordinate":
        return
    lead_id = assignee.get("lead_id")
    notify(
        lead_id,
        "completed",
        "Task completed",
        "%s completed: %s" % (assignee["name"], task["title"]),
        task_id=task["id"],
        actor_id=actor_id,
        url=url_for("reports.report"),
    )


# --------------------------------------------------------------------------
# Routes
# --------------------------------------------------------------------------
@notifications_bp.route("/notifications")
@login_required
def inbox():
    items = storage.get_notifications(current_user.id)
    storage.mark_all_read(current_user.id)
    return render_template("notifications.html", notifications=items)


@notifications_bp.route("/push/subscribe", methods=["POST"])
@login_required
def subscribe():
    sub = request.get_json(silent=True)
    if not sub or not sub.get("endpoint"):
        return jsonify({"error": "invalid subscription"}), 400
    storage.add_subscription(current_user.id, sub)
    return jsonify({"ok": True})


@notifications_bp.route("/push/unsubscribe", methods=["POST"])
@login_required
def unsubscribe():
    data = request.get_json(silent=True) or {}
    endpoint = data.get("endpoint")
    if endpoint:
        storage.remove_subscription(endpoint)
    return jsonify({"ok": True})
