"""
Core infrastructure: database, config, email, and shared utilities.
"""

import hashlib
import hmac
import os
import random
import secrets
from contextvars import ContextVar
from datetime import datetime, timedelta
from functools import wraps
from pathlib import Path

import aiosqlite
import resend
from dotenv import load_dotenv
from quart import g, make_response, render_template, request, session

# Load .env from repo root first (created by `make secrets-decrypt-dev`),
# fall back to web/.env for legacy local dev setups.
_repo_root = Path(__file__).parent.parent.parent.parent
load_dotenv(_repo_root / ".env", override=False)
load_dotenv(_repo_root / "web" / ".env", override=False)

# =============================================================================
# Configuration
# =============================================================================


class Config:
    APP_NAME: str = os.getenv("APP_NAME", "BeanFlows")
    SECRET_KEY: str = os.getenv("SECRET_KEY", "change-me-in-production")

    # Feature flags (set at template time via copier; mirror copier.yml questions)
    ENABLE_CMS: bool = True
    ENABLE_DAAS: bool = True
    ENABLE_DIRECTORY: bool = False
    ENABLE_LEADS: bool = False
    BUSINESS_MODEL: str = "saas"
    BASE_URL: str = os.getenv("BASE_URL", "http://localhost:5001")
    DEBUG: bool = os.getenv("DEBUG", "false").lower() == "true"

    DATABASE_PATH: str = os.getenv("DATABASE_PATH", "data/app.db")

    MAGIC_LINK_EXPIRY_MINUTES: int = int(os.getenv("MAGIC_LINK_EXPIRY_MINUTES", "15"))
    SESSION_LIFETIME_DAYS: int = int(os.getenv("SESSION_LIFETIME_DAYS", "30"))

    PAYMENT_PROVIDER: str = "paddle"

    PADDLE_API_KEY: str = os.getenv("PADDLE_API_KEY", "")
    PADDLE_WEBHOOK_SECRET: str = os.getenv("PADDLE_WEBHOOK_SECRET", "")
    PADDLE_ENVIRONMENT: str = os.getenv("PADDLE_ENVIRONMENT", "sandbox")
    PADDLE_PRICES: dict = {
        "starter": os.getenv("PADDLE_PRICE_STARTER", ""),
        "pro": os.getenv("PADDLE_PRICE_PRO", ""),
    }

    UMAMI_SCRIPT_URL: str = os.getenv("UMAMI_SCRIPT_URL", "")
    UMAMI_WEBSITE_ID: str = os.getenv("UMAMI_WEBSITE_ID", "")

    RESEND_API_KEY: str = os.getenv("RESEND_API_KEY", "")
    EMAIL_FROM: str = os.getenv("EMAIL_FROM", "hello@example.com")

    ADMIN_EMAILS: list[str] = [
        e.strip().lower()
        for e in os.getenv("ADMIN_EMAILS", "hendrik@beanflow.coffee,simon@beanflows.coffee").split(
            ","
        )
        if e.strip()
    ]

    RATE_LIMIT_REQUESTS: int = int(os.getenv("RATE_LIMIT_REQUESTS", "100"))
    RATE_LIMIT_WINDOW: int = int(os.getenv("RATE_LIMIT_WINDOW", "60"))

    WAITLIST_MODE: bool = os.getenv("WAITLIST_MODE", "false").lower() == "true"
    RESEND_AUDIENCE_WAITLIST: str = os.getenv("RESEND_AUDIENCE_WAITLIST", "")

    PLAN_FEATURES: dict = {
        "free": ["basic"],
        "starter": ["basic", "export"],
        "pro": ["basic", "export", "api", "priority_support"],
    }

    PLAN_LIMITS: dict = {
        "free": {"items": 100, "api_calls": 0},
        "starter": {"items": 1000, "api_calls": 10000},
        "pro": {"items": -1, "api_calls": -1},  # -1 = unlimited
    }


config = Config()

# =============================================================================
# Database
# =============================================================================

_db: aiosqlite.Connection | None = None


async def init_db(path: str = None) -> None:
    """Initialize database connection with WAL mode."""
    global _db
    db_path = path or config.DATABASE_PATH
    Path(db_path).parent.mkdir(parents=True, exist_ok=True)

    _db = await aiosqlite.connect(db_path)
    _db.row_factory = aiosqlite.Row

    await _db.execute("PRAGMA journal_mode=WAL")
    await _db.execute("PRAGMA foreign_keys=ON")
    await _db.execute("PRAGMA busy_timeout=5000")
    await _db.execute("PRAGMA synchronous=NORMAL")
    await _db.execute("PRAGMA cache_size=-64000")
    await _db.execute("PRAGMA temp_store=MEMORY")
    await _db.execute("PRAGMA mmap_size=268435456")
    await _db.commit()


async def close_db() -> None:
    """Close database connection."""
    global _db
    if _db:
        await _db.execute("PRAGMA wal_checkpoint(TRUNCATE)")
        await _db.close()
        _db = None


async def get_db() -> aiosqlite.Connection:
    """Get database connection."""
    if _db is None:
        await init_db()
    return _db


async def fetch_one(sql: str, params: tuple = ()) -> dict | None:
    """Fetch a single row as dict."""
    db = await get_db()
    async with db.execute(sql, params) as cursor:
        row = await cursor.fetchone()
        return dict(row) if row else None


async def fetch_all(sql: str, params: tuple = ()) -> list[dict]:
    """Fetch all rows as list of dicts."""
    db = await get_db()
    async with db.execute(sql, params) as cursor:
        rows = await cursor.fetchall()
        return [dict(row) for row in rows]


async def execute(sql: str, params: tuple = ()) -> int:
    """Execute SQL and return lastrowid."""
    db = await get_db()
    async with db.execute(sql, params) as cursor:
        await db.commit()
        return cursor.lastrowid


async def execute_many(sql: str, params_list: list[tuple]) -> None:
    """Execute SQL for multiple parameter sets."""
    db = await get_db()
    await db.executemany(sql, params_list)
    await db.commit()


class transaction:
    """Async context manager for transactions."""

    async def __aenter__(self):
        self.db = await get_db()
        return self.db

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            await self.db.commit()
        else:
            await self.db.rollback()
        return False


# =============================================================================
# Email
# =============================================================================

EMAIL_ADDRESSES = {
    "transactional": f"{config.APP_NAME} <{config.EMAIL_FROM}>",
}


async def send_email(
    to: str,
    subject: str,
    html: str,
    text: str = None,
    from_addr: str = None,
    template: str = None,
) -> bool:
    """Send email via Resend SDK and log to email_log table."""
    if not config.RESEND_API_KEY:
        print(f"[EMAIL] Would send to {to}: {subject}")
        await execute(
            "INSERT INTO email_log (recipient, subject, template, status, created_at) VALUES (?, ?, ?, 'dev_skip', ?)",
            (to, subject, template, datetime.utcnow().isoformat()),
        )
        return True

    resend.api_key = config.RESEND_API_KEY
    provider_id = None
    error_msg = None
    try:
        result = resend.Emails.send(
            {
                "from": from_addr or config.EMAIL_FROM,
                "to": to,
                "subject": subject,
                "html": html,
                "text": text or html,
            }
        )
        provider_id = result.get("id") if isinstance(result, dict) else None
    except Exception as e:
        error_msg = str(e)
        print(f"[EMAIL] Error sending to {to}: {e}")

    await execute(
        """INSERT INTO email_log (recipient, subject, template, status, provider_id, error, created_at)
           VALUES (?, ?, ?, ?, ?, ?, ?)""",
        (
            to,
            subject,
            template,
            "error" if error_msg else "sent",
            provider_id,
            error_msg,
            datetime.utcnow().isoformat(),
        ),
    )
    return error_msg is None


# =============================================================================
# CSRF Protection
# =============================================================================


def get_csrf_token() -> str:
    """Get or create CSRF token for current session."""
    if "csrf_token" not in session:
        session["csrf_token"] = secrets.token_urlsafe(32)
    return session["csrf_token"]


def validate_csrf_token(token: str) -> bool:
    """Validate CSRF token."""
    return token and secrets.compare_digest(token, session.get("csrf_token", ""))


def csrf_protect(f):
    """Decorator to require valid CSRF token for POST requests."""

    @wraps(f)
    async def decorated(*args, **kwargs):
        if request.method == "POST":
            form = await request.form
            token = form.get("csrf_token") or request.headers.get("X-CSRF-Token")
            if not validate_csrf_token(token):
                return {"error": "Invalid CSRF token"}, 403
        return await f(*args, **kwargs)

    return decorated


# =============================================================================
# Rate Limiting (SQLite-based)
# =============================================================================


async def check_rate_limit(key: str, limit: int = None, window: int = None) -> tuple[bool, dict]:
    """
    Check if rate limit exceeded. Returns (is_allowed, info).
    Uses SQLite for storage - no Redis needed.
    """
    limit = limit or config.RATE_LIMIT_REQUESTS
    window = window or config.RATE_LIMIT_WINDOW
    now = datetime.utcnow()
    window_start = now - timedelta(seconds=window)

    # Clean old entries and count recent
    await execute(
        "DELETE FROM rate_limits WHERE key = ? AND timestamp < ?", (key, window_start.isoformat())
    )

    result = await fetch_one(
        "SELECT COUNT(*) as count FROM rate_limits WHERE key = ? AND timestamp > ?",
        (key, window_start.isoformat()),
    )
    count = result["count"] if result else 0

    info = {
        "limit": limit,
        "remaining": max(0, limit - count - 1),
        "reset": int((window_start + timedelta(seconds=window)).timestamp()),
    }

    if count >= limit:
        return False, info

    # Record this request
    await execute("INSERT INTO rate_limits (key, timestamp) VALUES (?, ?)", (key, now.isoformat()))

    return True, info


def rate_limit(limit: int = None, window: int = None, key_func=None):
    """Decorator for rate limiting routes."""

    def decorator(f):
        @wraps(f)
        async def decorated(*args, **kwargs):
            if key_func:
                key = key_func()
            else:
                key = f"ip:{request.remote_addr}"

            allowed, info = await check_rate_limit(key, limit, window)

            if not allowed:
                response = {"error": "Rate limit exceeded", **info}
                return response, 429

            return await f(*args, **kwargs)

        return decorated

    return decorator


# =============================================================================
# Request ID Tracking
# =============================================================================

request_id_var: ContextVar[str] = ContextVar("request_id", default="")


def get_request_id() -> str:
    """Get current request ID."""
    return request_id_var.get()


def setup_request_id(app):
    """Setup request ID middleware."""

    @app.before_request
    async def set_request_id():
        rid = request.headers.get("X-Request-ID") or secrets.token_hex(8)
        request_id_var.set(rid)
        g.request_id = rid

    @app.after_request
    async def add_request_id_header(response):
        response.headers["X-Request-ID"] = get_request_id()
        return response


# =============================================================================
# Webhook Signature Verification
# =============================================================================


def verify_hmac_signature(payload: bytes, signature: str, secret: str) -> bool:
    """Verify HMAC-SHA256 webhook signature."""
    expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature, expected)


# =============================================================================
# Soft Delete Helpers
# =============================================================================


async def soft_delete(table: str, id: int) -> bool:
    """Mark record as deleted."""
    result = await execute(
        f"UPDATE {table} SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL",
        (datetime.utcnow().isoformat(), id),
    )
    return result > 0


async def restore(table: str, id: int) -> bool:
    """Restore soft-deleted record."""
    result = await execute(f"UPDATE {table} SET deleted_at = NULL WHERE id = ?", (id,))
    return result > 0


async def hard_delete(table: str, id: int) -> bool:
    """Permanently delete record."""
    result = await execute(f"DELETE FROM {table} WHERE id = ?", (id,))
    return result > 0


async def purge_deleted(table: str, days: int = 30) -> int:
    """Purge records deleted more than X days ago."""
    cutoff = (datetime.utcnow() - timedelta(days=days)).isoformat()
    return await execute(
        f"DELETE FROM {table} WHERE deleted_at IS NOT NULL AND deleted_at < ?", (cutoff,)
    )


# =============================================================================
# Waitlist
# =============================================================================


def waitlist_gate(template: str, **extra_context):
    """Intercept GET requests when WAITLIST_MODE=true and render the waitlist template."""

    def decorator(f):
        @wraps(f)
        async def wrapper(*args, **kwargs):
            if config.WAITLIST_MODE and request.method == "GET":
                ctx = {}
                for k, v in extra_context.items():
                    ctx[k] = v() if callable(v) else v
                return await render_template(template, **ctx)
            return await f(*args, **kwargs)

        return wrapper

    return decorator


async def capture_waitlist_email(
    email: str,
    intent: str = "signup",
    source: str = None,
    plan: str = None,
    ip_address: str = None,
) -> bool:
    """Insert email into waitlist (INSERT OR IGNORE) and enqueue confirmation email."""
    result = await execute(
        """
        INSERT OR IGNORE INTO waitlist (email, intent, source, plan, ip_address, created_at)
        VALUES (?, ?, ?, ?, ?, ?)
        """,
        (email.lower(), intent, source, plan, ip_address, datetime.utcnow().isoformat()),
    )

    if result:
        from .worker import enqueue

        await enqueue("send_waitlist_confirmation", {"email": email})

        if config.RESEND_AUDIENCE_WAITLIST and config.RESEND_API_KEY:
            try:
                resend.api_key = config.RESEND_API_KEY
                resend.Contacts.create(
                    {
                        "email": email,
                        "audience_id": config.RESEND_AUDIENCE_WAITLIST,
                        "unsubscribed": False,
                    }
                )
            except Exception as e:
                print(f"[WAITLIST] Resend audience error: {e}")

        return True
    return False


# =============================================================================
# A/B Testing
# =============================================================================


def ab_test(experiment: str, variants: tuple = ("control", "treatment")):
    """Assign visitor to an A/B test variant via cookie, tag Umami pageviews."""

    def decorator(f):
        @wraps(f)
        async def wrapper(*args, **kwargs):
            cookie_key = f"ab_{experiment}"
            assigned = request.cookies.get(cookie_key)
            if assigned not in variants:
                assigned = random.choice(variants)

            g.ab_variant = assigned
            g.ab_tag = f"{experiment}-{assigned}"

            response = await make_response(await f(*args, **kwargs))
            response.set_cookie(cookie_key, assigned, max_age=30 * 24 * 60 * 60)
            return response

        return wrapper

    return decorator


# =============================================================================
# Feature Flags (DB-backed, admin-toggleable)
# =============================================================================


async def is_flag_enabled(name: str, default: bool = False) -> bool:
    """Check if a feature flag is enabled. Falls back to default if not found."""
    row = await fetch_one("SELECT enabled FROM feature_flags WHERE name = ?", (name,))
    if row is None:
        return default
    return bool(row["enabled"])


async def set_flag(name: str, enabled: bool, description: str = None) -> None:
    """Create or update a feature flag."""
    await execute(
        """INSERT INTO feature_flags (name, enabled, description, updated_at)
           VALUES (?, ?, ?, ?)
           ON CONFLICT(name) DO UPDATE SET enabled = excluded.enabled,
           updated_at = excluded.updated_at""",
        (name, int(enabled), description, datetime.utcnow().isoformat()),
    )


async def get_all_flags() -> list[dict]:
    """Return all feature flags ordered by name."""
    return await fetch_all("SELECT * FROM feature_flags ORDER BY name")


def feature_gate(flag_name: str, fallback_template: str, **extra_context):
    """Gate a route behind a feature flag; renders fallback on GET, 403 on POST."""

    def decorator(f):
        @wraps(f)
        async def decorated(*args, **kwargs):
            if not await is_flag_enabled(flag_name):
                if request.method == "GET":
                    ctx = {k: v() if callable(v) else v for k, v in extra_context.items()}
                    return await render_template(fallback_template, **ctx)
                return {"error": "Feature not available"}, 403
            return await f(*args, **kwargs)

        return decorated

    return decorator
