# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Progressive Web App - Phase 3

Mobile-friendly chat interface as an installable PWA.

Features:
- Installable on iOS/Android home screen
- Offline message queue
- Push notifications (via Web Push API)
- Responsive design
- Session-based authentication

Run:
    python -m familiar.pwa --port 5002

Or integrate with admin dashboard on same server.
"""

from __future__ import annotations

import asyncio  # noqa: F401
import json
import logging
import os
import secrets
from datetime import datetime, timedelta  # noqa: F401
from functools import wraps
from pathlib import Path  # noqa: F401
from typing import Any, Dict, List, Optional  # noqa: F401

from flask import (
    Blueprint,
    Flask,
    Response,
    flash,
    g,
    jsonify,
    redirect,
    render_template,
    request,
    send_from_directory,
    session,
    url_for,
)

logger = logging.getLogger(__name__)

# Create Flask app
app = Flask(__name__, static_folder="static", template_folder="templates")
app.secret_key = os.environ.get("FAMILIAR_PWA_SECRET", secrets.token_hex(32))

# VAPID keys for push notifications (generate once and store)
VAPID_PUBLIC_KEY = os.environ.get("VAPID_PUBLIC_KEY", "")
VAPID_PRIVATE_KEY = os.environ.get("VAPID_PRIVATE_KEY", "")
VAPID_CLAIMS = {"sub": "mailto:admin@familiar.local"}

# Optional: pywebpush for push notifications
try:
    from pywebpush import WebPushException, webpush

    HAS_WEBPUSH = True
except ImportError:
    HAS_WEBPUSH = False
    logger.warning("pywebpush not installed - push notifications disabled")

# Import user management
try:
    from familiar.core.users import User, UserRole, get_user_manager  # noqa: F401

    HAS_USER_MANAGEMENT = True
except ImportError:
    HAS_USER_MANAGEMENT = False

# Import agent
try:
    from familiar.core import Agent, create_agent  # noqa: F401

    HAS_AGENT = True
except ImportError:
    HAS_AGENT = False


# ============================================================
# GLOBALS
# ============================================================

# Global agent instance (initialized on first request)
_agent = None

# Push subscription storage (in production, use database)
_push_subscriptions: Dict[str, Dict] = {}


def get_agent():
    """Get or create agent instance."""
    global _agent
    if _agent is None and HAS_AGENT:
        _agent = create_agent()
    return _agent


# ============================================================
# AUTHENTICATION
# ============================================================


def get_current_user() -> Optional[User]:
    """Get currently logged-in user."""
    if not HAS_USER_MANAGEMENT:
        return None

    if "user_token" not in session:
        return None

    manager = get_user_manager()
    return manager.authenticate_session(session["user_token"])


def login_required(f):
    """Decorator requiring login."""

    @wraps(f)
    def decorated(*args, **kwargs):
        user = get_current_user()
        if not user:
            if request.is_json:
                return jsonify({"error": "Authentication required"}), 401
            return redirect(url_for("pwa.login"))
        g.user = user
        return f(*args, **kwargs)

    return decorated


# ============================================================
# BLUEPRINT
# ============================================================

pwa_bp = Blueprint("pwa", __name__, template_folder="templates", static_folder="static")


# ---- Static PWA Files ----


@pwa_bp.route("/manifest.json")
def manifest():
    """PWA manifest for installability."""
    manifest_data = {
        "name": "Familiar Assistant",
        "short_name": "Familiar",
        "description": "Your AI assistant",
        "start_url": "/",
        "display": "standalone",
        "background_color": "#6B21A8",
        "theme_color": "#6B21A8",
        "orientation": "portrait-primary",
        "icons": [
            {
                "src": url_for("pwa.static", filename="icons/icon-192.png"),
                "sizes": "192x192",
                "type": "image/png",
                "purpose": "any maskable",
            },
            {
                "src": url_for("pwa.static", filename="icons/icon-512.png"),
                "sizes": "512x512",
                "type": "image/png",
                "purpose": "any maskable",
            },
        ],
        "categories": ["productivity", "utilities"],
        "screenshots": [],
        "shortcuts": [
            {"name": "New Chat", "url": "/?new=1", "description": "Start a new conversation"}
        ],
    }
    return jsonify(manifest_data)


@pwa_bp.route("/sw.js")
def service_worker():
    """Service worker for offline support."""
    return send_from_directory(pwa_bp.static_folder, "sw.js", mimetype="application/javascript")


# ---- Main Pages ----


@pwa_bp.route("/")
def index():
    """Main chat interface."""
    user = get_current_user()
    if not user:
        return redirect(url_for("pwa.login"))

    return render_template("chat.html", user=user, vapid_public_key=VAPID_PUBLIC_KEY)


@pwa_bp.route("/login", methods=["GET", "POST"])
def login():
    """Login page."""
    if request.method == "POST":
        email = request.form.get("email", "").strip().lower()

        if not email:
            flash("Email is required", "error")
            return render_template("pwa_login.html")

        if not HAS_USER_MANAGEMENT:
            flash("User management not available", "error")
            return render_template("pwa_login.html")

        manager = get_user_manager()
        user = manager.get_user_by_email(email)

        if not user or not user.is_active:
            flash("No active account found", "error")
            return render_template("pwa_login.html")

        # Create magic link
        token = manager.create_magic_link(email)
        link = url_for("pwa.verify", token=token, _external=True)

        # In production, send email. For dev, show link.
        flash(f"Login link (dev): {link}", "info")
        logger.info(f"PWA magic link for {email}: {link}")

        return render_template("pwa_login_sent.html", email=email)

    return render_template("pwa_login.html")


@pwa_bp.route("/verify")
def verify():
    """Verify magic link."""
    token = request.args.get("token")

    if not token or not HAS_USER_MANAGEMENT:
        flash("Invalid login link", "error")
        return redirect(url_for("pwa.login"))

    manager = get_user_manager()
    user = manager.verify_magic_link(token)

    if not user:
        flash("Invalid or expired link", "error")
        return redirect(url_for("pwa.login"))

    # Create session
    session_token = manager.create_session(
        user, ip_address=request.remote_addr, user_agent=request.user_agent.string
    )

    session["user_token"] = session_token
    session.permanent = True

    return redirect(url_for("pwa.index"))


@pwa_bp.route("/logout")
def logout():
    """Log out."""
    if "user_token" in session and HAS_USER_MANAGEMENT:
        manager = get_user_manager()
        manager.invalidate_session(session["user_token"])

    session.clear()
    return redirect(url_for("pwa.login"))


# ---- Chat API ----


@pwa_bp.route("/api/chat", methods=["POST"])
@login_required
def api_chat():
    """
    Send message to agent.

    Request:
        {"message": "Hello", "conversation_id": "optional"}

    Response:
        {"response": "Hi there!", "conversation_id": "abc123"}
    """
    data = request.get_json()

    if not data or "message" not in data:
        return jsonify({"error": "Message required"}), 400

    message = data["message"].strip()
    conversation_id = data.get("conversation_id", "default")

    if not message:
        return jsonify({"error": "Empty message"}), 400

    agent = get_agent()
    if not agent:
        return jsonify({"error": "Agent not available"}), 503

    try:
        # Get user context
        from familiar.core.users import UserContext

        user_context = UserContext(user=g.user) if HAS_USER_MANAGEMENT else None

        # Process message
        response = agent.chat(
            message=message,
            user_id=g.user.id if g.user else "pwa_guest",
            channel="pwa",
            user_context=user_context,
        )

        return jsonify(
            {
                "response": response,
                "conversation_id": conversation_id,
                "timestamp": datetime.utcnow().isoformat(),
            }
        )

    except Exception as e:
        logger.error(f"PWA chat error: {e}")
        return jsonify({"error": str(e)}), 500


@pwa_bp.route("/api/chat/stream", methods=["POST"])
@login_required
def api_chat_stream():
    """
    Stream response from agent using Server-Sent Events.

    Request:
        {"message": "Hello"}

    Response:
        text/event-stream with chunks
    """
    data = request.get_json()

    if not data or "message" not in data:
        return jsonify({"error": "Message required"}), 400

    message = data["message"].strip()

    agent = get_agent()
    if not agent:
        return jsonify({"error": "Agent not available"}), 503

    def generate():
        try:
            from familiar.core.users import UserContext

            user_context = UserContext(user=g.user) if HAS_USER_MANAGEMENT else None

            for event in agent.chat_stream(
                message=message,
                user_id=g.user.id if g.user else "pwa_guest",
                channel="pwa",
                user_context=user_context,
            ):
                if hasattr(event, "text") and event.text:
                    yield f"data: {json.dumps({'text': event.text})}\n\n"

            yield f"data: {json.dumps({'done': True})}\n\n"

        except Exception as e:
            yield f"data: {json.dumps({'error': str(e)})}\n\n"

    return Response(
        generate(),
        mimetype="text/event-stream",
        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
    )


# ---- Push Notifications ----


@pwa_bp.route("/api/push/subscribe", methods=["POST"])
@login_required
def push_subscribe():
    """
    Subscribe to push notifications.

    Request:
        {"subscription": {endpoint, keys: {p256dh, auth}}}
    """
    if not HAS_WEBPUSH:
        return jsonify({"error": "Push not available"}), 501

    data = request.get_json()
    subscription = data.get("subscription")

    if not subscription:
        return jsonify({"error": "Subscription required"}), 400

    # Store subscription
    user_id = g.user.id if g.user else "anonymous"
    _push_subscriptions[user_id] = subscription

    logger.info(f"Push subscription for {user_id}")

    return jsonify({"success": True})


@pwa_bp.route("/api/push/unsubscribe", methods=["POST"])
@login_required
def push_unsubscribe():
    """Unsubscribe from push notifications."""
    user_id = g.user.id if g.user else "anonymous"

    if user_id in _push_subscriptions:
        del _push_subscriptions[user_id]

    return jsonify({"success": True})


@pwa_bp.route("/api/push/test", methods=["POST"])
@login_required
def push_test():
    """Send test push notification."""
    if not HAS_WEBPUSH or not VAPID_PRIVATE_KEY:
        return jsonify({"error": "Push not configured"}), 501

    user_id = g.user.id if g.user else "anonymous"
    subscription = _push_subscriptions.get(user_id)

    if not subscription:
        return jsonify({"error": "Not subscribed"}), 400

    try:
        webpush(
            subscription_info=subscription,
            data=json.dumps(
                {
                    "title": "Familiar",
                    "body": "Push notifications are working!",
                    "icon": "/static/icons/icon-192.png",
                }
            ),
            vapid_private_key=VAPID_PRIVATE_KEY,
            vapid_claims=VAPID_CLAIMS,
        )
        return jsonify({"success": True})

    except WebPushException as e:
        logger.error(f"Push error: {e}")
        return jsonify({"error": str(e)}), 500


# ---- User API ----


@pwa_bp.route("/api/user")
@login_required
def api_user():
    """Get current user info."""
    return jsonify(
        {
            "id": g.user.id,
            "email": g.user.email,
            "name": g.user.name,
            "role": g.user.role.value if hasattr(g.user.role, "value") else str(g.user.role),
        }
    )


# ---- Offline Support ----


@pwa_bp.route("/api/ping")
def api_ping():
    """Simple ping for connectivity check."""
    return jsonify({"status": "ok", "timestamp": datetime.utcnow().isoformat()})


# ============================================================
# PUSH NOTIFICATION HELPER
# ============================================================


def send_push_notification(user_id: str, title: str, body: str, data: Dict = None):
    """
    Send push notification to a user.

    Args:
        user_id: User ID to notify
        title: Notification title
        body: Notification body
        data: Optional extra data
    """
    if not HAS_WEBPUSH or not VAPID_PRIVATE_KEY:
        return False

    subscription = _push_subscriptions.get(user_id)
    if not subscription:
        return False

    try:
        payload = {
            "title": title,
            "body": body,
            "icon": "/static/icons/icon-192.png",
            "data": data or {},
        }

        webpush(
            subscription_info=subscription,
            data=json.dumps(payload),
            vapid_private_key=VAPID_PRIVATE_KEY,
            vapid_claims=VAPID_CLAIMS,
        )
        return True

    except Exception as e:
        logger.error(f"Push notification failed: {e}")
        return False


# ============================================================
# REGISTER BLUEPRINT
# ============================================================

app.register_blueprint(pwa_bp)


# ============================================================
# MAIN
# ============================================================


def run_pwa(host: str = "0.0.0.0", port: int = 5002, debug: bool = False):
    """Run the PWA server."""
    logger.info(f"Starting PWA on http://{host}:{port}")
    app.run(host=host, port=port, debug=debug)


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Familiar PWA")
    parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
    parser.add_argument("--port", type=int, default=5002, help="Port to bind to")
    parser.add_argument("--debug", action="store_true", help="Enable debug mode")

    args = parser.parse_args()

    logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)

    run_pwa(host=args.host, port=args.port, debug=args.debug)
