import fcntl
import os
from dotenv import load_dotenv
_env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "data", ".env")
load_dotenv(_env_path)

from flask import Flask, request, session, abort, render_template
from werkzeug.middleware.proxy_fix import ProxyFix
import secrets
import traceback

from app.models import init_db, get_db
from app.routes.api import api_bp
from app.routes.admin import admin_bp
from app.routes.site import site_bp
from app.routes.auth import auth_bp
from app.routes.user_sites import user_sites_bp
from app.routes.billing import billing_bp
from app.routes.email import email_bp
from app.routes.dashboard import dashboard_bp
from app.routes.agent_api import agent_api_bp
from app.routes.user_settings import user_settings_bp
from app.routes.templates import templates_bp
from app.routes.teams import team_bp
from app.routes.invites import invite_bp
from app.routes.integrations import integrations_bp, export_bp
from app.routes.versions import versions_bp
from app.routes.documents import doc_bp
from app.routes.chains import chains_bp
from app.routes.agent_io import agent_io
from app.routes.referral import referral_bp
from app.routes.api_docs import api_docs_bp
from app.routes.webhooks import resend_bp
from app.routes.tracking import tracking_bp
from app.routes.actions import actions_bp
from app.routes.agents import agents_bp
from app.routes.agent_protocol import agent_protocol_bp
from app.routes.agent_build import agent_build_bp
from app.services.logging import log


_worker_lock_path = "/tmp/agentforms-workers.lock"


def _start_background_workers():
    """Only the first gunicorn worker starts background threads."""
    lock_fd = open(_worker_lock_path, "w")
    try:
        fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
        # We got the lock — we are the first worker
        lock_fd.write(str(os.getpid()))
        lock_fd.flush()
        return True
    except BlockingIOError:
        # Another worker already has the lock
        lock_fd.close()
        return False


def create_app():
    app = Flask(__name__)
    secret_key = os.environ.get("SECRET_KEY")
    if not secret_key:
        raise RuntimeError(
            "SECRET_KEY environment variable is required. "
            "Generate one with: python -c 'import secrets; print(secrets.token_hex(32))'"
        )
    app.secret_key = secret_key
    app.permanent_session_lifetime = 14 * 24 * 3600  # 2 weeks for "remember me"

    # ─── Global payload size limit ────────────────────────────────────────────
    # Prevent DoS via oversized requests (Phase 2)
    app.config["MAX_CONTENT_LENGTH"] = 1 * 1024 * 1024  # 1MB

    # ─── Session cookie hardening ──────────────────────────────────────────────
    # Secure: True behind HTTPS (Caddy/TLS)
    app.config["SESSION_COOKIE_SECURE"] = not app.config.get("TESTING", False)
    app.config["SESSION_COOKIE_HTTPONLY"] = True
    app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
    # Use a custom session cookie name (not the default "session")
    app.config["SESSION_COOKIE_NAME"] = "__Host-_af_session"

    # ─── CSRF protection ────────────────────────────────────────────────────────
    # API endpoints are exempted — they use tokens, not form submissions.
    # Routes that legitimately don't need CSRF: Stripe webhook, login.
    _csrf_exempt_paths = {
        "/billing/stripe/webhook",
        "/auth/magic",  # Login entry point — no session to protect
        "/auth/login",  # JSON login — no session yet
        "/auth/register",  # Registration entry point — no session yet
        "/documents/generate",  # JSON API — authenticated sessions skip CSRF (line 62-64)
        "/documents/history",  # JSON API
        "/documents/gallery",  # JSON API
        "/api/waitlist",  # Public waitlist — anonymous users, no session
        "/api/beacon",  # Public beacon — anonymous SDK impression tracking
        "/api/sessions",  # Public sessions — anonymous SDK analytics
        "/resend/webhook",  # Resend webhook — external events
        "/tracking/bounce",  # Bounce webhook — external SMTP relay
        "/api/v2/templates",  # Email templates API — API key auth, no session
        "/api/v2/email/settings",  # Email settings API — API key auth
        "/api/v2/email/domain/verify",  # Domain verification API — API key auth
        "/api/v2/email/domain/check",  # Domain check API — API key auth
        "/api/submit",  # Public form submission — token auth, no session
        "/api/v2/agent/deploy",  # Agent I/O — API key auth
        "/api/v2/agent/document",  # Agent I/O — API key auth
        "/api/v2/agent/submissions",  # Agent I/O — API key auth
        "/api/v2/agent/forms",  # Agent I/O — API key auth
    }

    @app.before_request
    def csrf_protect():
        if request.method in ("GET", "HEAD", "OPTIONS", "TRACE"):
            return None
        # Skip CSRF in test mode — tests use client sessions without CSRF tokens
        if app.config.get("TESTING", False):
            return None
        if request.path in _csrf_exempt_paths:
            return None
        # Form actions API — dynamic paths, JSON API with session auth
        path = request.path
        parts = path.strip("/").split("/")
        if (len(parts) == 3 and parts[0] == "sites" and parts[2] == "actions") or \
           (len(parts) >= 2 and parts[0] == "actions" and parts[1].isdigit()):
            if "user_id" in session:
                return None
        # API key auth (Bearer afk_* or X-API-Key) bypasses CSRF — token-based auth is CSRF-safe
        auth_header = request.headers.get("Authorization", "")
        api_key_header = request.headers.get("X-API-Key", "")
        if auth_header.startswith("Bearer afk_") or api_key_header.startswith("af_"):
            return None
        # Ensure a CSRF token exists in session
        if "csrf_token" not in session:
            session["csrf_token"] = secrets.token_hex(32)
        # JSON API requests from authenticated sessions skip CSRF (session cookie is SameSite+Secure)
        if request.content_type and "application/json" in request.content_type:
            if "user_id" in session:
                return None
        # Verify token from hidden form field or header
        token = request.form.get("csrf_token") or request.headers.get("X-CSRF-Token")
        if not token or not secrets.compare_digest(token.encode("utf-8"), session["csrf_token"].encode("utf-8")):
            return abort(403, description="CSRF token missing or invalid")

    # Inject csrf_token + CSP nonce into all templates
    @app.context_processor
    def inject_csrf():
        if "csrf_token" not in session:
            session["csrf_token"] = secrets.token_hex(32)
        nonce = getattr(request, "csp_nonce", None) or secrets.token_urlsafe(16)
        return {"csrf_token": session.get("csrf_token", ""), "csp_nonce": nonce}

    # Generate per-request CSP nonce
    @app.before_request
    def _generate_csp_nonce():
        request.csp_nonce = secrets.token_urlsafe(16)

    # ─── Custom template filters ──────────────────────────────────────────────
    # nl2br_escape — escape HTML first, then convert newlines to <br> (XSS-safe)
    from markupsafe import Markup
    @app.template_filter("nl2br")
    def nl2br_filter(value):
        if not value:
            return ""
        from markupsafe import escape
        escaped = escape(str(value))
        return Markup(str(escaped).replace("\n", "<br>\n"))

    # Proxy fix — trust Caddy reverse proxy
    app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)

    # ─── Security headers ─────────────────────────────────────────────────────
    @app.after_request
    def _security_headers(response):
        # Allow document previews to be iframed
        if request.path.startswith("/documents/") and (request.path.endswith("/preview") or request.path.endswith("/embed")):
            return response
        # Content Security Policy — nonce-based (no unsafe-inline/unsafe-eval)
        nonce = getattr(request, "csp_nonce", None) or secrets.token_urlsafe(16)
        response.headers["Content-Security-Policy"] = (
            "default-src 'self'; "
            f"script-src 'self' 'nonce-{nonce}'; "
            f"style-src 'self' 'nonce-{nonce}'; "
            "img-src 'self' data: https:; "
            "font-src 'self' data:; "
            "connect-src 'self'; "
            "frame-ancestors 'none';"
        )
        # HSTS — enforce HTTPS for 1 year, no subdomains
        response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
        # Prevent clickjacking
        response.headers["X-Frame-Options"] = "DENY"
        # Prevent MIME sniffing
        response.headers["X-Content-Type-Options"] = "nosniff"
        # Referrer policy
        response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
        # Permissions Policy — restrict unused browser features
        response.headers["Permissions-Policy"] = (
            "camera=(), microphone=(), geolocation=(), "
            "payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()"
        )
        return response

    # ─── Static SEO/Compliance pages ──────────────────────────────────────────
    @app.route("/robots.txt")
    def robots_txt():
        base = os.environ.get("APP_URL", "https://agentforms.io")
        return f"User-agent: *\nAllow: /\nDisallow: /admin/\nDisallow: /settings/\nDisallow: /billing/\n\nSitemap: {base}/sitemap.xml\n", 200, {"Content-Type": "text/plain"}

    @app.route("/favicon.ico")
    def favicon():
        from flask import send_from_directory, current_app
        return send_from_directory(current_app.static_folder, "favicon.svg", mimetype="image/svg+xml")

    @app.route("/.well-known/security.txt")
    def security_txt():
        base = os.environ.get("APP_URL", "https://agentforms.io")
        return f"Contact: mailto:security@agentforms.io\nExpires: 2027-06-12T00:00:00.000Z\nEncryption: {base}/.well-known/security.txt.asc\nPreferred-Languages: en\nPolicy: {base}/security-policy\nCanonical: {base}/.well-known/security.txt\n", 200, {"Content-Type": "text/plain"}

    @app.route("/sitemap.xml")
    def sitemap():
        """Dynamic sitemap including blog posts and all public routes."""
        import os, glob
        from datetime import datetime

        urls = []
        base = os.environ.get("APP_URL", "https://agentforms.io")

        # Static high-priority pages
        static_pages = [
            ("/", 1.0, "daily"),
            ("/features", 0.8, "weekly"),
            ("/pricing", 0.9, "weekly"),
            ("/changelog", 0.6, "weekly"),
            ("/auth/register", 0.7, "monthly"),
            ("/auth/login", 0.7, "monthly"),
            ("/api/docs", 0.5, "monthly"),
            ("/wp-plugin", 0.4, "monthly"),
            ("/privacy", 0.3, "monthly"),
            ("/terms", 0.3, "monthly"),
        ]

        now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S+00:00")
        for path, priority, changefreq in static_pages:
            urls.append(
                f"    <url>"
                f"<loc>{base}{path}</loc>"
                f"<lastmod>{now}</lastmod>"
                f"<changefreq>{changefreq}</changefreq>"
                f"<priority>{priority}</priority>"
                f"</url>"
            )

        xml = '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
        xml += "\n".join(urls)
        xml += "\n</urlset>"
        return xml, 200, {"Content-Type": "application/xml"}


    @app.route("/changelog")
    def changelog_page():
        """Public changelog page."""
        return render_template("changelog.html")

    # ─── Error handling ──────────────────────────────────────────────────────
    @app.errorhandler(404)
    def not_found(e):
        return render_template("error_404.html"), 404

    @app.errorhandler(403)
    def forbidden(e):
        return render_template("error_403.html"), 403

    @app.errorhandler(413)
    def payload_too_large(e):
        """Handle oversized request payloads (Phase 2)."""
        if request.is_json:
            return {"error": "Payload too large (max 1MB)"}, 413
        return "Payload too large", 413

    @app.errorhandler(500)
    def internal_error(e):
        """Handle 5xx errors with alerting."""
        from app.services.alerting import alert
        from app.services.logging import log

        tb = traceback.format_exc()
        log.error("app", f"Internal server error: {e}", path=request.path, method=request.method)
        alert.error(
            f"500 Internal Server Error",
            path=request.path,
            method=request.method,
            error=str(e),
        )
        return render_template("error_500.html"), 500

    @app.errorhandler(502)
    def bad_gateway(e):
        """Handle 502 errors with alerting."""
        from app.services.alerting import alert
        from app.services.logging import log

        log.error("app", "Bad gateway", path=request.path, method=request.method)
        alert.error(
            f"502 Bad Gateway",
            path=request.path,
            method=request.method,
            error=str(e),
        )
        return {"error": "Bad gateway"}, 502

    @app.errorhandler(503)
    def service_unavailable(e):
        """Handle 503 errors with alerting."""
        from app.services.alerting import alert
        from app.services.logging import log

        log.error("app", "Service unavailable", path=request.path, method=request.method)
        alert.error(
            f"503 Service Unavailable",
            path=request.path,
            method=request.method,
            error=str(e),
        )
        return {"error": "Service unavailable"}, 503

    # ─── API Documentation ─────────────────────────────────────────────────────
    @app.route("/api/docs")
    def api_docs():
        return render_template("api_docs.html")

    # ─── Health endpoints ──────────────────────────────────────────────────────
    # /health — basic liveness probe (Docker healthcheck)
    @app.route("/health")
    def health():
        return {"status": "ok", "service": "relay"}, 200

    @app.route("/health/ready")
    def health_ready():
        """Readiness probe — checks DB + Redis connectivity."""
        from flask import jsonify
        errors = []
        # Check DB
        conn = None
        try:
            conn = get_db()
            conn.execute("SELECT 1")
        except Exception as e:
            errors.append({"db": str(e)})
        finally:
            if conn:
                conn.close()
        # Check Redis
        redis_url = os.environ.get("REDIS_URL")
        if redis_url:
            try:
                import redis as redis_lib
                r = redis_lib.from_url(redis_url)
                r.ping()
            except Exception as e:
                errors.append({"redis": str(e)})
        if errors:
            return jsonify({"status": "degraded", "service": "relay", "errors": errors}), 503
        return jsonify({"status": "ok", "service": "relay"}), 200

    @app.route("/health/redis")
    def health_redis():
        """Check Redis connectivity — used by worker healthcheck."""
        from flask import jsonify
        redis_url = os.environ.get("REDIS_URL")
        if not redis_url:
            return jsonify({"status": "unavailable", "reason": "REDIS_URL not set"}), 503
        try:
            import redis as redis_lib
            r = redis_lib.from_url(redis_url)
            r.ping()
            return jsonify({"status": "ok", "service": "redis"}), 200
        except Exception as e:
            return jsonify({"status": "error", "service": "redis", "error": str(e)}), 503

    @app.route("/health/worker")
    def health_worker():
        """Check RQ worker health via Redis heartbeat."""
        from flask import jsonify
        redis_url = os.environ.get("REDIS_URL")
        if not redis_url:
            return jsonify({"status": "unavailable", "reason": "REDIS_URL not set"}), 503
        try:
            import redis as redis_lib
            r = redis_lib.from_url(redis_url)
            # Check RQ registry for active workers
            import rq
            from rq.registry import StartedJobRegistry
            registry = StartedJobRegistry(connection=r)
            active_jobs = len(registry.get_job_ids())
            # Check worker heartbeat key
            heartbeat_key = "agentforms:worker:heartbeat"
            last_heartbeat = r.get(heartbeat_key)
            if last_heartbeat:
                import time as _time
                last_ts = float(last_heartbeat)
                age = _time.time() - last_ts
                if age > 180:  # stale if > 3 min
                    return jsonify({
                        "status": "degraded",
                        "service": "worker",
                        "active_jobs": active_jobs,
                        "heartbeat_age_seconds": round(age, 1),
                    }), 200
                return jsonify({
                    "status": "ok",
                    "service": "worker",
                    "active_jobs": active_jobs,
                    "heartbeat_age_seconds": round(age, 1),
                }), 200
            return jsonify({
                "status": "no_heartbeat",
                "service": "worker",
                "active_jobs": active_jobs,
            }), 200
        except Exception as e:
            return jsonify({"status": "error", "service": "worker", "error": str(e)}), 503

    # Init DB (includes migrations)
    init_db()

    # Background workers
    from app.models import seed_monthly_usage
    seed_monthly_usage()  # Ensure all users have current month rows

    # ─── Redis setup ──────────────────────────────────────────────────────────

    redis_client = None
    redis_url = os.environ.get("REDIS_URL")
    if redis_url:
        try:
            import redis as redis_lib
            redis_client = redis_lib.from_url(redis_url)
            redis_client.ping()
            log.info("relay", "Redis connected", url=redis_url)
        except Exception as e:
            log.warning("relay", "Redis unavailable", error=str(e))

    # Connect rate limiter to Redis
    from app.services.ratelimit import limiter
    if redis_client:
        limiter.connect(redis_client)

    from app.services.webhook import start_retry_worker
    from app.services.backup import start_backup_worker, start_vacuum_worker
    from app.services.usage_alerts import start_usage_alert_worker
    from app.services.campaign_emails import start_reminder_worker, start_ab_test_worker

    # Only the first gunicorn worker starts background threads (prevents duplicate workers)
    if _start_background_workers():
        log.info("app", f"Starting background workers (PID {os.getpid()})")
        start_retry_worker()
        start_backup_worker()
        start_vacuum_worker()
        start_usage_alert_worker()
        start_reminder_worker()
        start_ab_test_worker()
    else:
        log.info("app", f"Skipping background workers (PID {os.getpid()}) — another worker has the lock")

    # ─── Rate limiting middleware ──────────────────────────────────────────────
    # API endpoints: 60 req/min per IP
    # Auth endpoints: 10 req/min per IP (brute-force protection)
    AUTH_RATE_LIMIT = 10
    API_RATE_LIMIT = 60

    @app.before_request
    def rate_limit_check():
        path = request.path

        # Auth endpoints — stricter limit
        if path.startswith("/auth/") or path.startswith("/settings/"):
            ip_key = f"rate:ip:{request.remote_addr}:auth"
            allowed, remaining, reset_at = limiter.check(
                ip_key, max_requests=AUTH_RATE_LIMIT, window=60
            )
            request.rate_limit_info = {
                "limit": AUTH_RATE_LIMIT,
                "remaining": remaining,
                "reset": int(reset_at),
            }
            if not allowed:
                from flask import jsonify as jf
                resp = jf({"error": "Too many requests. Try again later."})
                resp.status_code = 429
                resp.headers["Retry-After"] = "60"
                return resp

        # API endpoints — per-IP limit
        if path.startswith("/api/"):
            ip_key = f"rate:ip:{request.remote_addr}:api"
            allowed, remaining, reset_at = limiter.check(
                ip_key, max_requests=API_RATE_LIMIT, window=60
            )
            request.rate_limit_info = {
                "limit": API_RATE_LIMIT,
                "remaining": remaining,
                "reset": int(reset_at),
            }
            if not allowed:
                from flask import jsonify as jf
                resp = jf({"error": "Too many requests. Try again later."})
                resp.status_code = 429
                resp.headers["Retry-After"] = "60"
                return resp

        # Per-user API rate limiting (authenticated users)
        # Prevents a compromised account from spamming
        if "user_id" in session and path.startswith("/api/"):
            user_key = f"rate:user:{session['user_id']}:api"
            user_allowed, user_remaining, user_reset = limiter.check(
                user_key, max_requests=120, window=60
            )
            if not user_allowed:
                from flask import jsonify as jf
                resp = jf({
                    "error": "User rate limit exceeded. Try again later.",
                    "usage": {"current": 120, "max": 120, "window": 60},
                })
                resp.status_code = 429
                resp.headers["Retry-After"] = "60"
                return resp

    @app.after_request
    def add_rate_limit_headers(response):
        """Add X-RateLimit-* headers to API responses."""
        if hasattr(request, "rate_limit_info") and request.rate_limit_info:
            info = request.rate_limit_info
            response.headers["X-RateLimit-Limit"] = str(info["limit"])
            response.headers["X-RateLimit-Remaining"] = str(info["remaining"])
            response.headers["X-RateLimit-Reset"] = str(info["reset"])
        return response

    # Register blueprints — site first (catches / before admin)
    app.register_blueprint(templates_bp)  # /api/v2/templates — template marketplace
    app.register_blueprint(agent_api_bp)  # /api/v2/ — agent API (must be before /api/)
    app.register_blueprint(site_bp)
    app.register_blueprint(api_bp)
    app.register_blueprint(auth_bp)
    app.register_blueprint(user_sites_bp)
    app.register_blueprint(billing_bp)
    app.register_blueprint(email_bp)
    app.register_blueprint(admin_bp, url_prefix='/admin')
    app.register_blueprint(user_settings_bp)
    app.register_blueprint(team_bp)
    app.register_blueprint(invite_bp)
    app.register_blueprint(integrations_bp)
    app.register_blueprint(export_bp)
    app.register_blueprint(versions_bp)
    app.register_blueprint(doc_bp)
    app.register_blueprint(referral_bp)
    app.register_blueprint(api_docs_bp)  # /api/docs/
    app.register_blueprint(resend_bp)  # /resend/webhook
    app.register_blueprint(tracking_bp)  # /tracking/open, /tracking/click
    app.register_blueprint(actions_bp)  # /api/actions — form action CRUD
    app.register_blueprint(agent_io)  # /api/v2/agent — deploy forms, generate docs on demand
    app.register_blueprint(agents_bp)  # /api/v2/agents — Phase C agent registry
    app.register_blueprint(agent_protocol_bp)  # /api/v2/agent-protocol — Phase C protocol
    app.register_blueprint(dashboard_bp)
    app.register_blueprint(chains_bp)
    app.register_blueprint(agent_build_bp)  # /api/chains — Phase B: AI chain suggestions

    return app


# ─── Graceful shutdown ────────────────────────────────────────────────────────
# On SIGTERM/SIGINT (container restart, deploy), drain in-flight requests
# before exiting. Gunicorn handles this natively; standalone mode needs it.
import signal
_shutting_down = False


def _graceful_shutdown(signum, frame):
    global _shutting_down
    _shutting_down = True
    log.info("shutdown", f"Received {signal.Signals(signum).name} — draining in-flight requests...")


signal.signal(signal.SIGTERM, _graceful_shutdown)
signal.signal(signal.SIGINT, _graceful_shutdown)


# Module-level app instance — used by tests and Gunicorn
app = create_app()

if __name__ == "__main__":
    port = int(os.environ.get("PORT", 5060))
    debug = os.environ.get("DEBUG", "false").lower() == "true"
    log.info("relay", "Starting", port=port)
    app.run(host="0.0.0.0", port=port, debug=debug)
