"""Student Bazaar application factory.

Builds and configures the Flask application following the Application Factory
pattern. Responsibilities are limited to wiring the application together:
loading configuration, binding extensions, ensuring upload directories exist,
registering feature blueprints and installing error handlers.

Business logic lives inside each feature module's service layer, not here.
"""

from __future__ import annotations

import os

from flask import Flask, jsonify, redirect, render_template, request, url_for
from flask_login import login_required

from app.config import config
from app.extensions import csrf, db, login_manager, mail, migrate


def create_app(config_name: str | None = None) -> Flask:
    """Create and configure a Flask application instance.

    Args:
        config_name: Name of the configuration to use ("development",
            "testing" or "production"). Falls back to the ``FLASK_CONFIG``
            environment variable and then to the development configuration.

    Returns:
        A fully configured :class:`~flask.Flask` application.
    """
    config_name = config_name or os.environ.get("FLASK_CONFIG", "default")

    app = Flask(__name__)
    app.config.from_object(config[config_name])

    _ensure_upload_directories(app)
    _register_extensions(app)
    _register_models()
    _register_blueprints(app)
    _register_core_routes(app)
    _register_error_handlers(app)
    _register_cli(app)

    return app


def _register_cli(app: Flask) -> None:
    """Register custom Flask CLI commands."""
    from app.cli import register_cli

    register_cli(app)


def _register_core_routes(app: Flask) -> None:
    """Register top-level routes that do not belong to a feature blueprint."""

    @app.route("/notifications")
    @login_required
    def notifications_redirect():
        return redirect(url_for("notifications.list_notifications"))

    @app.context_processor
    def inject_globals():
        from flask_login import current_user

        from app.advertisements.services import (
            get_active_advertisement,
            get_active_advertisements,
        )
        from app.models.base import utcnow
        from app.premium.permissions import can_view_without_ads

        # Premium members get an ad-free experience: the template-facing ad
        # helpers return nothing for them, so every ad slot renders empty without
        # any per-template conditional. Free users are unaffected.
        ad_free = can_view_without_ads(current_user)

        def active_ad(*args, **kwargs):
            return None if ad_free else get_active_advertisement(*args, **kwargs)

        def active_ads(*args, **kwargs):
            return [] if ad_free else get_active_advertisements(*args, **kwargs)

        return {
            "now_year": utcnow().year,
            # Reusable, data-driven ad helpers available to every template.
            "active_ads": active_ads,
            "active_ad": active_ad,
        }

    # Premium capability checks, callable from any template/macro.
    from app.premium.permissions import can_feature_listing, should_show_premium_badge

    app.jinja_env.globals["show_premium_badge"] = should_show_premium_badge
    app.jinja_env.globals["can_feature_listing"] = can_feature_listing

    # Centralised, user-facing advertisement-placement label (e.g. ``chat`` →
    # "Chat Unlock"). Reuses the enum's ``display_name`` so templates never
    # hardcode placement names; used where only the raw value/string is on hand.
    from app.models.enums import AdvertisementPlacement

    def _placement_label(value):
        try:
            return AdvertisementPlacement(value).display_name
        except (ValueError, TypeError):
            return str(value or "").title()

    app.jinja_env.filters["placement_label"] = _placement_label

    # Featured-listing helpers (expiry-aware) so templates never compute expiry.
    from app.marketplace.services import (
        featured_days_remaining,
        is_listing_currently_featured,
    )

    app.jinja_env.globals["is_featured_now"] = is_listing_currently_featured
    app.jinja_env.globals["featured_days_left"] = featured_days_remaining

    # Premium expiry helper (days remaining) so templates never compute it.
    from app.premium.services import premium_days_remaining

    app.jinja_env.globals["premium_days_left"] = premium_days_remaining

    # Whether a route exists — lets templates link only to available pages and
    # gracefully disable shortcuts whose management page isn't built yet.
    app.jinja_env.globals["has_endpoint"] = lambda name: name in app.view_functions


def _register_models() -> None:
    """Import models so they register with SQLAlchemy metadata.

    Required for Flask-Migrate autogenerate to detect the tables and for ORM
    queries at runtime. The import is local to avoid import-time cycles.
    """
    from app import models  # noqa: F401


def _ensure_upload_directories(app: Flask) -> None:
    """Create the upload directories defined in configuration if absent."""
    for key in (
        "UPLOAD_FOLDER",
        "PROFILE_PICTURE_FOLDER",
        "LISTING_IMAGE_FOLDER",
        "ADVERTISEMENT_FOLDER",
        "ADVERTISEMENT_UPLOAD_FOLDER",
        "PREMIUM_RECEIPT_FOLDER",
        "PREMIUM_QR_UPLOAD_FOLDER",
    ):
        directory = app.config.get(key)
        if directory:
            os.makedirs(directory, exist_ok=True)


def _register_extensions(app: Flask) -> None:
    """Bind all Flask extensions to the application."""

    print("MAIL_SERVER:", app.config.get("MAIL_SERVER"))
    print("MAIL_PORT:", app.config.get("MAIL_PORT"))
    print("MAIL_USERNAME:", repr(app.config.get("MAIL_USERNAME")))
    print("MAIL_PASSWORD:", repr(app.config.get("MAIL_PASSWORD")))

    db.init_app(app)
    migrate.init_app(app, db)
    mail.init_app(app)
    csrf.init_app(app)
    login_manager.init_app(app)
    _warn_on_unconfigured_mail(app)


def _warn_on_unconfigured_mail(app: Flask) -> None:
    """Fail loud at startup (log a warning) when outbound email is clearly
    misconfigured, so silent OTP-delivery failures can't recur unnoticed.

    Skipped under testing (where mail is intentionally suppressed)."""
    if app.config.get("TESTING"):
        return
    server = (app.config.get("MAIL_SERVER") or "").lower()
    problems = []
    if not server or server in ("localhost", "127.0.0.1") or "example.com" in server:
        problems.append(f"MAIL_SERVER={app.config.get('MAIL_SERVER')!r} looks like a placeholder")
    if app.config.get("MAIL_SUPPRESS_SEND"):
        problems.append("MAIL_SUPPRESS_SEND is True (emails will NOT be sent)")
    if not app.config.get("MAIL_USERNAME"):
        problems.append("MAIL_USERNAME is empty")
    if problems:
        app.logger.warning(
            "Email delivery may be misconfigured: %s. Verify your .env; "
            "run `flask send-test-email <address>` to diagnose.",
            "; ".join(problems),
        )


def _register_blueprints(app: Flask) -> None:
    """Register every feature blueprint.

    Blueprints are imported lazily inside the factory so that module-level code
    in feature packages can safely import shared extensions without circular
    import issues.
    """
    from app.activity import activity_bp
    from app.admin import admin_bp
    from app.advertisements import advertisements_bp
    from app.auth import auth_bp
    from app.chat import chat_bp
    from app.main import main_bp
    from app.marketplace import marketplace_bp
    from app.meetup import meetup_bp
    from app.negotiation import negotiation_bp
    from app.notifications import notifications_bp
    from app.payments import payments_bp
    from app.payments.webhook import razorpay_webhook
    from app.premium import premium_bp
    from app.reviews import reviews_bp

    app.register_blueprint(main_bp)
    app.register_blueprint(advertisements_bp, url_prefix="/promotions")
    app.register_blueprint(auth_bp)
    app.register_blueprint(marketplace_bp, url_prefix="/app")
    app.register_blueprint(negotiation_bp, url_prefix="/app/offers")
    app.register_blueprint(chat_bp, url_prefix="/app/chat")
    app.register_blueprint(meetup_bp, url_prefix="/app/meetups")
    app.register_blueprint(notifications_bp, url_prefix="/app/notifications")
    app.register_blueprint(premium_bp, url_prefix="/app/premium")
    app.register_blueprint(payments_bp, url_prefix="/app/payments")
    app.register_blueprint(reviews_bp, url_prefix="/app/reviews")
    app.register_blueprint(activity_bp, url_prefix="/app/activity")
    app.register_blueprint(admin_bp, url_prefix="/admin")

    # The gateway webhook is a server-to-server POST (no session/CSRF token); it is
    # authenticated by its own signature instead. Exempt only this one endpoint.
    csrf.exempt(razorpay_webhook)


def _register_error_handlers(app: Flask) -> None:
    """Install centralized error handlers.

    Responses adapt to the request: JSON for API/AJAX clients and a rendered
    template for browser navigation. Unexpected server errors are logged while
    keeping sensitive details out of the response.
    """

    def wants_json() -> bool:
        if request.path.startswith("/api"):
            return True
        return request.accept_mimetypes.best == "application/json"

    def respond(message: str, status_code: int):
        if wants_json():
            return jsonify(success=False, message=message), status_code
        template = f"errors/{status_code}.html"
        try:
            return render_template(template, message=message), status_code
        except Exception:
            # Error templates are introduced with the presentation layer; until
            # then fall back to a plain-text response.
            return message, status_code

    @app.errorhandler(400)
    def bad_request(_error):
        return respond("Bad request.", 400)

    @app.errorhandler(401)
    def unauthorized(_error):
        return respond("Authentication is required.", 401)

    @app.errorhandler(403)
    def forbidden(_error):
        return respond("You do not have permission to perform this action.", 403)

    @app.errorhandler(404)
    def not_found(_error):
        return respond("The requested resource was not found.", 404)

    @app.errorhandler(429)
    def too_many_requests(_error):
        return respond("Too many requests. Please try again later.", 429)

    @app.errorhandler(500)
    def internal_server_error(error):
        db.session.rollback()
        app.logger.exception("Unhandled application error: %s", error)
        return respond("An unexpected error occurred. Please try again later.", 500)
