"""
Registration and multi-step onboarding routes for ModuleDesk.

Flow:
  /register              — create org + user, send verification email
  /verify-email/<token>  — verify email address
  /onboard/api-key       — enter Addons API key (rate-limited)
  /onboard/modules       — review and toggle module list
  /onboard/plan          — choose plan tier
  /onboard/ai            — ModuleDesk AI vs BYOK
  /onboard/billing       — Stripe Checkout (skipped for free plan)
  /onboard/complete      — trigger first sync, redirect to inbox
"""

from __future__ import annotations

import datetime as dt
import hashlib
import json
import logging
import os
import re
import secrets

import bcrypt
from flask import (
    render_template, redirect, url_for, request, flash, session as flask_session,
    jsonify, current_app, abort,
)
from flask_login import login_user, current_user, login_required
from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired

from . import onboarding_bp
from ..db import session_scope
from ..public_models import Organization, User, PublicAuditLog
from ..services.tenant_service import provision_tenant, make_schema_name
from ..services.auth_service import hash_password, verify_password

logger = logging.getLogger(__name__)

BUSINESS_CARE_PRODUCT_TYPE = "business_care"


# ── Rate limiting helpers (Redis) ─────────────────────────────────────────────

def _get_redis():
    import redis
    return redis.from_url(os.getenv("REDIS_URL", "redis://localhost:6379/0"))


def _ip_hash(ip: str) -> str:
    return hashlib.sha256(ip.encode()).hexdigest()[:32]


def _check_rate_limit(ip: str, email: str) -> tuple[bool, int]:
    """
    Check if this IP (or email) is rate-limited for API key validation.
    Returns (is_blocked, seconds_remaining).
    """
    try:
        r = _get_redis()
        ip_h = _ip_hash(ip)

        # Permanent block?
        if r.exists(f"addons_key_blocked:{ip_h}"):
            return True, -1  # -1 = contact support

        # Active lockout?
        lock_val = r.get(f"addons_key_lock:{ip_h}")
        if lock_val:
            ttl = r.ttl(f"addons_key_lock:{ip_h}")
            return True, max(0, ttl)

        return False, 0
    except Exception:
        return False, 0


def _record_fail(ip: str, email: str):
    """Record a failed API key attempt and set the appropriate lockout."""
    try:
        r = _get_redis()
        ip_h = _ip_hash(ip)

        fails_key = f"addons_key_fail:{ip_h}"
        count = r.incr(fails_key)
        r.expire(fails_key, 7 * 86400)  # track for 7 days

        if count >= 15:
            r.set(f"addons_key_blocked:{ip_h}", "1")  # permanent — contact support
            lockout = None
        elif count >= 12:
            lockout = 86400       # 24 hours
        elif count >= 9:
            lockout = 3600        # 1 hour
        elif count >= 6:
            lockout = 300         # 5 minutes
        elif count >= 3:
            lockout = 60          # 60 seconds
        else:
            lockout = None

        if lockout:
            r.setex(f"addons_key_lock:{ip_h}", lockout, "1")
    except Exception:
        pass


def _clear_rate_limit(ip: str):
    """Clear rate limit keys on successful API key validation."""
    try:
        r = _get_redis()
        ip_h = _ip_hash(ip)
        r.delete(f"addons_key_fail:{ip_h}", f"addons_key_lock:{ip_h}")
    except Exception:
        pass


# ── Email verification token helpers ─────────────────────────────────────────

def _make_verify_token(email: str) -> str:
    s = URLSafeTimedSerializer(current_app.secret_key)
    return s.dumps(email, salt="email-verify")


def _verify_token(token: str, max_age: int = 86400) -> str | None:
    """Return email from token or None if invalid/expired."""
    s = URLSafeTimedSerializer(current_app.secret_key)
    try:
        return s.loads(token, salt="email-verify", max_age=max_age)
    except (BadSignature, SignatureExpired):
        return None


# ── Registration ─────────────────────────────────────────────────────────────

@onboarding_bp.route("/register", methods=["GET", "POST"])
def register():
    if current_user.is_authenticated:
        return redirect(url_for("inbox"))

    error = None
    if request.method == "POST":
        org_name = request.form.get("org_name", "").strip()
        email = request.form.get("email", "").strip().lower()
        password = request.form.get("password", "")

        if not org_name or not email or not password:
            error = "All fields are required."
        elif len(password) < 8:
            error = "Password must be at least 8 characters."
        elif not re.match(r"^[^@]+@[^@]+\.[^@]+$", email):
            error = "Invalid email address."
        else:
            slug = _make_slug(org_name)
            schema_name = make_schema_name(slug)

            with session_scope() as session:
                # Check email uniqueness
                if session.query(User).filter_by(email=email).one_or_none():
                    error = "An account with this email already exists."
                elif session.query(Organization).filter_by(slug=slug).one_or_none():
                    error = "Organisation name taken — try a different name."
                else:
                    org = Organization(
                        name=org_name,
                        slug=slug,
                        schema_name=schema_name,
                        plan="free",
                        email_verified=False,
                        onboarding_complete=False,
                    )
                    session.add(org)
                    session.flush()

                    verify_token = _make_verify_token(email)
                    user = User(
                        org_id=org.id,
                        email=email,
                        password_hash=hash_password(password),
                        role="admin",
                        email_verified=False,
                        email_verify_token=verify_token,
                        email_verify_sent_at=dt.datetime.utcnow(),
                    )
                    session.add(user)
                    session.flush()

                    # Store org_id in session for onboarding steps
                    flask_session["onboarding_org_id"] = org.id
                    flask_session["onboarding_user_id"] = user.id

                    _send_verification_email(email, verify_token)
                    return redirect(url_for("onboarding.verify_email_sent"))

    return render_template("onboarding/register.html", error=error)


@onboarding_bp.route("/verify-email-sent")
def verify_email_sent():
    return render_template("onboarding/verify_email_sent.html")


@onboarding_bp.route("/verify-email/<token>")
def verify_email(token: str):
    email = _verify_token(token, max_age=86400)
    if not email:
        return render_template("onboarding/verify_failed.html")

    with session_scope() as session:
        user = session.query(User).filter_by(email=email).one_or_none()
        if not user:
            return render_template("onboarding/verify_failed.html")

        user.email_verified = True
        user.email_verify_token = None
        org = user.org
        org.email_verified = True
        flask_session["onboarding_org_id"] = org.id
        flask_session["onboarding_user_id"] = user.id

    return redirect(url_for("onboarding.onboard_api_key"))


# ── Onboarding Step 1: Addons API Key ────────────────────────────────────────

@onboarding_bp.route("/onboard/api-key", methods=["GET", "POST"])
def onboard_api_key():
    org_id = flask_session.get("onboarding_org_id")
    if not org_id:
        return redirect(url_for("onboarding.register"))

    error = None
    lockout_seconds = 0

    if request.method == "POST":
        api_key = request.form.get("api_key", "").strip()
        ip = request.remote_addr

        # Check rate limit
        is_blocked, seconds = _check_rate_limit(ip, "")
        if is_blocked:
            if seconds == -1:
                error = "Too many failed attempts. Please contact support."
            else:
                error = f"Too many failed attempts. Wait {seconds} seconds."
                lockout_seconds = seconds
        elif not api_key:
            error = "Please enter your Addons API key."
        else:
            valid, modules = _validate_addons_api_key(api_key)
            if not valid:
                _record_fail(ip, "")
                error = "Invalid API key — check it and try again."
            else:
                _clear_rate_limit(ip)
                # Encrypt and store the API key
                with session_scope() as session:
                    org = session.query(Organization).filter_by(id=org_id).one_or_none()
                    if org:
                        org.addons_api_key_encrypted = _encrypt(api_key)

                # Cache module list in session for next step
                flask_session["onboarding_modules"] = modules
                return redirect(url_for("onboarding.onboard_modules"))

    return render_template("onboarding/api_key.html", error=error, lockout_seconds=lockout_seconds)


# ── Onboarding Step 2: Module List ───────────────────────────────────────────

@onboarding_bp.route("/onboard/modules", methods=["GET", "POST"])
def onboard_modules():
    org_id = flask_session.get("onboarding_org_id")
    if not org_id:
        return redirect(url_for("onboarding.register"))

    modules = flask_session.get("onboarding_modules", [])
    # Exclude Business Care products
    modules = [m for m in modules if m.get("product_type") != BUSINESS_CARE_PRODUCT_TYPE]

    if request.method == "POST":
        # User may have toggled billing_active — store selections
        selected = request.form.getlist("billing_active")
        for m in modules:
            m["billing_active"] = str(m["id"]) in selected
        flask_session["onboarding_modules"] = modules
        return redirect(url_for("onboarding.onboard_plan"))

    return render_template("onboarding/modules.html", modules=modules)


# ── Onboarding Step 3: Plan Selection ────────────────────────────────────────

@onboarding_bp.route("/onboard/plan", methods=["GET", "POST"])
def onboard_plan():
    org_id = flask_session.get("onboarding_org_id")
    if not org_id:
        return redirect(url_for("onboarding.register"))

    modules = flask_session.get("onboarding_modules", [])
    billable_count = sum(1 for m in modules if m.get("billing_active", True))

    from ..services.plan_service import PLAN_LIMITS, PLAN_FEATURES, PLAN_PRICE_CENTS

    if request.method == "POST":
        chosen_plan = request.form.get("plan", "free")
        if chosen_plan not in ("free", "basic", "pro"):
            chosen_plan = "free"
        flask_session["onboarding_plan"] = chosen_plan
        return redirect(url_for("onboarding.onboard_ai"))

    return render_template(
        "onboarding/plan.html",
        modules=modules,
        billable_count=billable_count,
        plan_limits=PLAN_LIMITS,
        plan_features=PLAN_FEATURES,
        plan_price_cents=PLAN_PRICE_CENTS,
    )


# ── Onboarding Step 4: AI Setup ──────────────────────────────────────────────

@onboarding_bp.route("/onboard/ai", methods=["GET", "POST"])
def onboard_ai():
    org_id = flask_session.get("onboarding_org_id")
    if not org_id:
        return redirect(url_for("onboarding.register"))

    error = None
    if request.method == "POST":
        ai_choice = request.form.get("ai_choice", "platform")
        byok_key = request.form.get("openai_key", "").strip()

        if ai_choice == "byok":
            if not byok_key:
                error = "Please paste your OpenAI API key."
            elif not _validate_openai_key(byok_key):
                error = "Could not validate the OpenAI key. Check it and try again."
            else:
                with session_scope() as session:
                    org = session.query(Organization).filter_by(id=org_id).one_or_none()
                    if org:
                        org.openai_api_key_encrypted = _encrypt(byok_key)

        if not error:
            chosen_plan = flask_session.get("onboarding_plan", "free")
            if chosen_plan == "free":
                return redirect(url_for("onboarding.onboard_complete"))
            return redirect(url_for("onboarding.onboard_billing"))

    return render_template("onboarding/ai_setup.html", error=error)


# ── Onboarding Step 5: Billing (Stripe Checkout) ─────────────────────────────

@onboarding_bp.route("/onboard/billing", methods=["GET", "POST"])
def onboard_billing():
    org_id = flask_session.get("onboarding_org_id")
    chosen_plan = flask_session.get("onboarding_plan", "free")
    if not org_id or chosen_plan == "free":
        return redirect(url_for("onboarding.onboard_complete"))

    modules = flask_session.get("onboarding_modules", [])
    billable_count = max(1, sum(1 for m in modules if m.get("billing_active", True)))

    if request.method == "POST":
        with session_scope() as session:
            org = session.query(Organization).filter_by(id=org_id).one_or_none()
            if org:
                from ..services.billing_service import create_stripe_customer, create_subscription
                if not org.stripe_customer_id:
                    create_stripe_customer(org)
                create_subscription(org, chosen_plan, billable_count)

        return redirect(url_for("onboarding.onboard_complete"))

    return render_template("onboarding/billing.html", plan=chosen_plan, billable_count=billable_count)


# ── Onboarding Complete ───────────────────────────────────────────────────────

@onboarding_bp.route("/onboard/complete")
def onboard_complete():
    org_id = flask_session.get("onboarding_org_id")
    user_id = flask_session.get("onboarding_user_id")
    chosen_plan = flask_session.get("onboarding_plan", "free")
    if not org_id or not user_id:
        return redirect(url_for("onboarding.register"))

    with session_scope() as session:
        org = session.query(Organization).filter_by(id=org_id).one_or_none()
        user = session.query(User).filter_by(id=user_id).one_or_none()
        if not org or not user:
            return redirect(url_for("onboarding.register"))

        if chosen_plan in ("basic", "pro"):
            org.plan = chosen_plan

        org.onboarding_complete = True

    # Provision the tenant schema (CREATE SCHEMA + Alembic migrations + seed)
    with session_scope() as session:
        org = session.query(Organization).filter_by(id=org_id).one_or_none()
        try:
            provision_tenant(org)
        except Exception as exc:
            logger.error("Tenant provisioning failed for org %s: %s", org_id, exc)

    # Trigger first sync as Celery task
    try:
        from ..tasks.sync_tasks import sync_threads
        with session_scope() as session:
            org = session.query(Organization).filter_by(id=org_id).one_or_none()
            if org:
                sync_threads.delay(org_id=org_id, schema=org.schema_name)
    except Exception as exc:
        logger.warning("Could not queue first sync for org %s: %s", org_id, exc)

    # Log the user in
    with session_scope() as session:
        user = session.query(User).filter_by(id=user_id).one_or_none()
        if user and user.org:
            from ..main import User as FlaskUser
            flask_user = FlaskUser({
                "id": user.id,
                "email": user.email,
                "role": user.role,
                "org_id": user.org_id,
                "schema_name": user.org.schema_name,
                "plan": user.org.plan,
                "ai_credits_used": user.org.ai_credits_used,
                "openai_api_key_encrypted": user.org.openai_api_key_encrypted,
                "org_is_suspended": user.org.is_suspended,
            })
            flask_session.permanent = True
            login_user(flask_user)

    # Clean up onboarding session keys
    for key in ("onboarding_org_id", "onboarding_user_id", "onboarding_plan", "onboarding_modules"):
        flask_session.pop(key, None)

    return render_template("onboarding/complete.html")


# ── Accept Invite ─────────────────────────────────────────────────────────────

@onboarding_bp.route('/accept-invite/<token>', methods=['GET', 'POST'])
def accept_invite(token: str):
    """Accept an email invite and set a password."""
    data = _verify_invite_token(token)
    if not data:
        return render_template('onboarding/accept_invite.html', error='This invite link is invalid or has expired.')

    email = data.get('email', '')
    org_id = data.get('org_id')

    with session_scope() as session:
        user = session.query(User).filter_by(email=email, org_id=org_id, email_verified=False).one_or_none()
        if not user:
            return render_template('onboarding/accept_invite.html', error='This invite has already been accepted or is no longer valid.')

        if request.method == 'POST':
            password = request.form.get('password', '')
            confirm = request.form.get('confirm_password', '')
            if len(password) < 8:
                return render_template('onboarding/accept_invite.html', token=token, email=email, error='Password must be at least 8 characters.')
            if password != confirm:
                return render_template('onboarding/accept_invite.html', token=token, email=email, error='Passwords do not match.')
            user.password_hash = hash_password(password)
            user.email_verified = True
            user.email_verify_token = None

    if request.method == 'POST':
        flash('Your account is set up. You can now log in.', 'success')
        return redirect(url_for('login'))

    return render_template('onboarding/accept_invite.html', token=token, email=email)


# ── Helpers ───────────────────────────────────────────────────────────────────

def _make_slug(org_name: str) -> str:
    """Turn an org name into a URL-safe slug."""
    slug = re.sub(r"[^a-z0-9]", "-", org_name.lower())
    slug = re.sub(r"-+", "-", slug).strip("-")
    return slug[:50] or "org"


def _encrypt(plaintext: str) -> bytes:
    """Encrypt a string using the platform Fernet key."""
    from cryptography.fernet import Fernet
    from ..config import config
    key = config.CREDENTIAL_ENCRYPTION_KEY
    if not key:
        return plaintext.encode()  # fallback (no key configured)
    f = Fernet(key.encode() if isinstance(key, str) else key)
    return f.encrypt(plaintext.encode())


def _validate_addons_api_key(api_key: str) -> tuple[bool, list]:
    """
    Validate an Addons API key by making a real API call.
    Returns (is_valid, module_list).
    module_list is a list of dicts with product info.
    """
    try:
        from ..providers.addons import ProviderAddons
        from ..config import config
        provider = ProviderAddons(api_key=api_key, base_url=config.ADDONS_API_BASE_URL)
        products = provider.get_products()
        if products is None:
            return False, []
        return True, [
            {
                "id": p.get("id_product") or p.get("id"),
                "name": p.get("name", ""),
                "product_type": p.get("type", ""),
                "is_online": p.get("active", True),
                "billing_active": True,
            }
            for p in (products if isinstance(products, list) else [])
        ]
    except Exception as exc:
        logger.warning("Addons API key validation failed: %s", exc)
        return False, []


def _validate_openai_key(api_key: str) -> bool:
    """Validate an OpenAI key by making a minimal test call."""
    try:
        import openai
        client = openai.OpenAI(api_key=api_key)
        client.models.list()
        return True
    except Exception:
        return False


def _make_invite_token(email: str, org_id: int) -> str:
    s = URLSafeTimedSerializer(current_app.secret_key)
    return s.dumps({"email": email, "org_id": org_id}, salt="invite")


def _verify_invite_token(token: str, max_age: int = 7 * 86400):
    s = URLSafeTimedSerializer(current_app.secret_key)
    try:
        return s.loads(token, salt="invite", max_age=max_age)
    except (BadSignature, SignatureExpired):
        return None


def _send_invite_email(email: str, token: str, invited_by_email: str):
    """Send invite email. Logs the URL if flask-mail is not configured."""
    accept_url = url_for('onboarding.accept_invite', token=token, _external=True)
    logger.info("Invite URL for %s: %s", email, accept_url)
    if "mail" not in current_app.extensions:
        return
    try:
        from flask_mail import Message
        mail = current_app.extensions["mail"]
        msg = Message(
            subject="You've been invited to ModuleDesk",
            recipients=[email],
            body=(
                f"Hi,\n\n{invited_by_email} has invited you to join their ModuleDesk team.\n\n"
                f"Accept your invitation here (link expires in 7 days):\n{accept_url}\n\n"
                f"If you didn't expect this, you can ignore this email."
            ),
        )
        mail.send(msg)
    except Exception as exc:
        logger.error("Failed to send invite email to %s: %s", email, exc)


def _send_verification_email(email: str, token: str):
    """Send email verification link. No-op if flask-mail not configured."""
    try:
        from flask import current_app
        from flask_mail import Message
        if "mail" not in current_app.extensions:
            logger.info("flask-mail not configured; verification link: /verify-email/%s", token)
            return
        mail = current_app.extensions["mail"]
        verify_url = url_for("onboarding.verify_email", token=token, _external=True)
        msg = Message(
            subject="Verify your ModuleDesk email",
            recipients=[email],
            body=f"Click to verify your email and complete registration:\n\n{verify_url}",
        )
        mail.send(msg)
    except Exception as exc:
        logger.error("Failed to send verification email to %s: %s", email, exc)
