"""Application configuration.

Configuration is environment-driven. Sensitive values (secret key, database
credentials, mail credentials) are read from environment variables, loaded from
a local ``.env`` file via python-dotenv during development.

Three environments are supported: development, testing and production. Each
inherits shared defaults from :class:`Config` and overrides only what differs.
"""

from __future__ import annotations

import os
from pathlib import Path
from urllib.parse import quote_plus

from dotenv import load_dotenv

# Project root (the directory that contains this ``app`` package).
BASE_DIR = Path(__file__).resolve().parent.parent

# Load environment variables from a project-level .env file when present.
load_dotenv(BASE_DIR / ".env", override=True)


def _build_database_uri() -> str:
    """Assemble the SQLAlchemy MySQL URI from individual environment values.

    A full ``DATABASE_URL`` takes precedence when provided; otherwise the URI is
    composed from the discrete ``DB_*`` variables so credentials can be managed
    independently in different environments.
    """
    explicit_url = os.environ.get("DATABASE_URL")
    if explicit_url:
        return explicit_url

    username = quote_plus(os.environ.get("DB_USERNAME", "root"))
    password = quote_plus(os.environ.get("DB_PASSWORD", ""))
    host = os.environ.get("DB_HOST", "127.0.0.1")
    port = os.environ.get("DB_PORT", "3306")
    name = os.environ.get("DB_NAME", "student_bazaar")

    return f"mysql+pymysql://{username}:{password}@{host}:{port}/{name}?charset=utf8mb4"


def _as_bool(value: str | None, default: bool = False) -> bool:
    """Interpret a string environment value as a boolean."""
    if value is None:
        return default
    return value.strip().lower() in {"1", "true", "yes", "on"}


class Config:
    """Base configuration shared across all environments."""

    # --- Core ---------------------------------------------------------------
    SECRET_KEY = os.environ.get("SECRET_KEY", "change-me-in-production")

    # --- Database (Flask-SQLAlchemy) ---------------------------------------
    SQLALCHEMY_DATABASE_URI = _build_database_uri()
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_ENGINE_OPTIONS = {
        # Recycle connections before MySQL's default wait_timeout to avoid
        # "server has gone away" errors on long-lived pools.
        "pool_pre_ping": True,
        "pool_recycle": 280,
    }

    # --- Sessions / cookies -------------------------------------------------
    SESSION_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SAMESITE = "Lax"
    # Enabled in production where HTTPS is enforced; see ProductionConfig.
    SESSION_COOKIE_SECURE = False
    REMEMBER_COOKIE_HTTPONLY = True
    REMEMBER_COOKIE_SAMESITE = "Lax"

    # --- CSRF (Flask-WTF) ---------------------------------------------------
    WTF_CSRF_ENABLED = True
    WTF_CSRF_TIME_LIMIT = None  # Token tied to the session lifetime.

    # --- Mail (Flask-Mail) --------------------------------------------------
    MAIL_SERVER = (os.environ.get("MAIL_SERVER", "localhost") or "").strip()
    MAIL_PORT = int(os.environ.get("MAIL_PORT", "587"))
    MAIL_USE_TLS = _as_bool((os.environ.get("MAIL_USE_TLS") or "").strip(), default=True)
    MAIL_USE_SSL = _as_bool((os.environ.get("MAIL_USE_SSL") or "").strip(), default=False)
    MAIL_USERNAME = (os.environ.get("MAIL_USERNAME") or "").strip() or None
    MAIL_PASSWORD = (os.environ.get("MAIL_PASSWORD") or "").strip() or None
    MAIL_DEFAULT_SENDER = (
    os.environ.get("MAIL_DEFAULT_SENDER", "Student Bazaar <no-reply@studentbazaar.local>")
    or ""
).strip()
    
    # Explicit so email delivery is never accidentally disabled outside tests.
    # (Flask-Mail otherwise infers this from TESTING; being explicit prevents a
    # stray test setting from leaking into development/production.)
    MAIL_SUPPRESS_SEND = _as_bool(os.environ.get("MAIL_SUPPRESS_SEND"), default=False)
    MAIL_DEBUG = _as_bool(os.environ.get("MAIL_DEBUG"), default=False)
    # Support address shown in emails and used as Reply-To (a monitored mailbox
    # improves deliverability vs a no-reply). Not Gmail-specific.
    SUPPORT_EMAIL = os.environ.get("SUPPORT_EMAIL", "support@studentbazaar.in")
    MAIL_REPLY_TO = os.environ.get("MAIL_REPLY_TO", SUPPORT_EMAIL)

    # --- Registration -------------------------------------------------------
    # How long a pending (pre-verification) registration survives before it is
    # auto-deleted. Users must confirm the emailed OTP within this window.
    TEMP_REGISTRATION_TTL_MINUTES = int(os.environ.get("TEMP_REGISTRATION_TTL_MINUTES", "10"))

    # --- File uploads -------------------------------------------------------
    UPLOAD_FOLDER = BASE_DIR / "uploads"
    PROFILE_PICTURE_FOLDER = UPLOAD_FOLDER / "profile_pictures"
    LISTING_IMAGE_FOLDER = UPLOAD_FOLDER / "listing_images"
    ADVERTISEMENT_FOLDER = UPLOAD_FOLDER / "advertisements"
    PREMIUM_RECEIPT_FOLDER = UPLOAD_FOLDER / "premium_receipts"
    ALLOWED_IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "webp"}
    # Largest allowed advertisement video upload (MP4/WebM/Ogg). Videos are far
    # larger than images, so the global request cap below must accommodate them.
    MAX_AD_VIDEO_SIZE = int(os.environ.get("MAX_AD_VIDEO_SIZE", str(50 * 1024 * 1024)))
    # Reject request bodies larger than the biggest allowed upload (+2 MB margin
    # for the surrounding multipart form) before they reach the handler.
    MAX_CONTENT_LENGTH = int(os.environ.get("MAX_CONTENT_LENGTH", str(MAX_AD_VIDEO_SIZE + 2 * 1024 * 1024)))

    # --- Advertisement admin uploads ---------------------------------------
    # Admin-managed advertisement images are served from the static folder so
    # the existing ad templates can render them directly via ``image_path``.
    ADVERTISEMENT_UPLOAD_FOLDER = BASE_DIR / "app" / "static" / "uploads" / "advertisements"
    # Public URL prefix corresponding to ADVERTISEMENT_UPLOAD_FOLDER.
    ADVERTISEMENT_UPLOAD_URL = "uploads/advertisements"
    ALLOWED_AD_IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "webp"}
    MAX_AD_IMAGE_SIZE = int(os.environ.get("MAX_AD_IMAGE_SIZE", str(2 * 1024 * 1024)))
    # Rewarded-ad video uploads share the advertisement upload folder/strategy.
    ALLOWED_AD_VIDEO_EXTENSIONS = {"mp4", "webm", "ogg"}
    # Default Display Weight for a new advertisement (the ``priority`` column). 100
    # = "normal"; when every ad shares it they get equal exposure under the
    # weighted-random selector. Weight controls probability, never ordering.
    DEFAULT_AD_PRIORITY = int(os.environ.get("DEFAULT_AD_PRIORITY", "100"))
    # Admin-uploaded Premium UPI QR image â€” served publicly from the static folder
    # (rendered directly on the premium payment page). Reuses the same secure image
    # save/validation helpers as advertisements (png/jpg/jpeg/webp, MAX_AD_IMAGE_SIZE).
    PREMIUM_QR_UPLOAD_FOLDER = BASE_DIR / "app" / "static" / "uploads" / "premium"
    PREMIUM_QR_UPLOAD_URL = "uploads/premium"

    # --- Email verification OTP --------------------------------------------
    # A single One-Time-Password system is shared by registration and the
    # login gate (see app/auth/otp_service.py). All limits are configuration â€”
    # never hard-coded in the service. Codes are numeric, hashed at rest, and
    # single-use.
    OTP_LENGTH = int(os.environ.get("OTP_LENGTH", "6"))
    OTP_EXPIRY_MINUTES = int(os.environ.get("OTP_EXPIRY_MINUTES", "5"))
    # Minimum wait between two "resend" requests for the same account.
    OTP_RESEND_COOLDOWN_SECONDS = int(os.environ.get("OTP_RESEND_COOLDOWN_SECONDS", "60"))
    # Maximum verification codes that may be issued to one account per hour.
    OTP_MAX_PER_HOUR = int(os.environ.get("OTP_MAX_PER_HOUR", "5"))
    # Wrong-code attempts allowed against the current code before it is
    # invalidated and a fresh code must be requested (brute-force guard).
    OTP_MAX_VERIFY_ATTEMPTS = int(os.environ.get("OTP_MAX_VERIFY_ATTEMPTS", "5"))

    # --- Login verification (OTP on untrusted devices) ---------------------
    # A verified user signing in from a device that isn't already trusted must
    # confirm an emailed one-time code before the session is established. Reuses
    # the same OTP framework via VerificationPurpose.LOGIN_VERIFICATION â€” no
    # second OTP system. Turn this off (env LOGIN_OTP_ENABLED=false) to fall back
    # to the classic password-only login; the automated test suite disables it so
    # existing credential-based logins stay deterministic (see TestingConfig).
    LOGIN_OTP_ENABLED = _as_bool(os.environ.get("LOGIN_OTP_ENABLED"), default=True)
    # Login codes are longer-lived than the (shorter) registration/reset codes
    # because the user has already proven the password. Per-purpose override of
    # the shared OTP_EXPIRY_MINUTES; other purposes are unaffected.
    LOGIN_OTP_EXPIRY_MINUTES = int(os.environ.get("LOGIN_OTP_EXPIRY_MINUTES", "10"))

    # --- Trusted devices ----------------------------------------------------
    # A confirmed login may mark the browser as trusted for this many days so the
    # OTP step is skipped on subsequent sign-ins. Only a salted hash of the device
    # token is ever stored; the raw token lives only in a secure cookie.
    TRUSTED_DEVICE_DAYS = int(os.environ.get("TRUSTED_DEVICE_DAYS", "30"))
    TRUSTED_DEVICE_COOKIE = os.environ.get("TRUSTED_DEVICE_COOKIE", "cc_trusted_device")

    # --- Login attempt lockout (Security_Design Â§7) ------------------------
    # Consecutive failed password attempts before the account is temporarily
    # locked, and how long the lock lasts. Reuses the existing
    # ``users.failed_login_attempts`` / ``users.locked_until`` columns. Generous
    # by default so only sustained brute force trips it.
    LOGIN_MAX_FAILED_ATTEMPTS = int(os.environ.get("LOGIN_MAX_FAILED_ATTEMPTS", "10"))
    LOGIN_LOCKOUT_MINUTES = int(os.environ.get("LOGIN_LOCKOUT_MINUTES", "15"))

    # --- Pagination ---------------------------------------------------------
    DEFAULT_PAGE_SIZE = 20

    # --- Chat / rewarded advertisements ------------------------------------
    # Free messages per conversation once counting begins (both students have
    # exchanged their first message). Matches the approved Chat design.
    FREE_MESSAGE_LIMIT = 10
    # Messages granted by watching one rewarded advertisement. Change this single
    # value to adjust the reward (e.g. 10). Rewarded ads stack indefinitely with
    # no per-conversation cap â€” add a limit here only if one is introduced later.
    MESSAGES_PER_REWARDED_AD = 5
    # Cooldown between rewarded advertisements, in seconds. 0 (default) means a
    # student may watch another ad immediately â€” the current approved behaviour.
    # Set a positive value later to gate ads behind a cooldown; the chat UI already
    # renders a live "next advertisement available in â€¦" countdown when > 0.
    REWARDED_AD_COOLDOWN_SECONDS = 0
    # How long the simulated rewarded advertisement plays, in seconds. The chat
    # modal reads this value dynamically (via a data attribute) to drive the
    # countdown and progress bar, so changing it here is the only edit needed â€”
    # no template or JS changes. Reward granting is unaffected (backend-only).
    REWARDED_AD_DURATION_SECONDS = 20
    # Lifetime of a signed rewarded-ad reward token, in seconds. The modal mints a
    # token when it opens; the claim must arrive within this window (default 5
    # minutes). The token proves the modal was legitimately issued â€” not that the
    # ad was watched â€” and is single-use to block replays.
    REWARDED_AD_TOKEN_TTL_SECONDS = 300
    # Rate limits for reward-related endpoints (per authenticated user, per window).
    # Deliberately generous â€” a normal user opens one modal and claims one reward
    # every ~20s countdown, well under these â€” so only automated abuse is blocked.
    REWARD_RATE_WINDOW_SECONDS = int(os.environ.get("REWARD_RATE_WINDOW_SECONDS", "60"))
    REWARD_TOKEN_RATE_MAX = int(os.environ.get("REWARD_TOKEN_RATE_MAX", "30"))   # token mints / window
    REWARD_CLAIM_RATE_MAX = int(os.environ.get("REWARD_CLAIM_RATE_MAX", "15"))   # reward claims / window
    REWARD_EVENT_RATE_MAX = int(os.environ.get("REWARD_EVENT_RATE_MAX", "90"))   # video beacons / window
    # Sponsored banner cadence in the marketplace catalog (every N listings).
    MARKETPLACE_AD_INTERVAL = 12

    # --- Payments (Razorpay) -----------------------------------------------
    # Gateway credentials. Read from the environment only â€” never hardcoded and
    # never sent to the browser (only the public KEY_ID is exposed to Checkout).
    RAZORPAY_KEY_ID = os.environ.get("RAZORPAY_KEY_ID", "")
    RAZORPAY_KEY_SECRET = os.environ.get("RAZORPAY_KEY_SECRET", "")
    RAZORPAY_WEBHOOK_SECRET = os.environ.get("RAZORPAY_WEBHOOK_SECRET", "")
    # ISO currency for premium orders. Plan *prices* come from the admin-configured
    # PremiumPlan rows (never from the browser); this is the currency they charge in.
    PREMIUM_CURRENCY = os.environ.get("PREMIUM_CURRENCY", "INR")
    # Reserved for future pricing rules (kept in config, not hardcoded downstream).
    PREMIUM_TAX_PERCENT = float(os.environ.get("PREMIUM_TAX_PERCENT", "0") or 0)
    PREMIUM_TRIAL_DAYS = int(os.environ.get("PREMIUM_TRIAL_DAYS", "0") or 0)
    # True only when both Razorpay keys are configured â€” gates the online upgrade
    # button so the manual flow remains the sole path until credentials are set.
    PAYMENTS_ENABLED = bool(RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET)


class DevelopmentConfig(Config):
    """Local development configuration."""

    DEBUG = True


class TestingConfig(Config):
    """Configuration for automated tests."""

    TESTING = True
    DEBUG = False
    # In-memory SQLite keeps the test suite fast and isolated from MySQL.
    SQLALCHEMY_DATABASE_URI = os.environ.get(
        "TEST_DATABASE_URL", "sqlite+pysqlite:///:memory:"
    )
    SQLALCHEMY_ENGINE_OPTIONS = {}
    WTF_CSRF_ENABLED = False
    # Do not attempt real SMTP delivery in tests; Flask-Mail records messages to
    # an outbox instead (assertable via ``mail.record_messages()``).
    MAIL_SUPPRESS_SEND = True
    # Existing tests authenticate with a plain ``POST /login`` and expect the
    # session to be established immediately. Keep the login-OTP gate off by
    # default under testing so those flows stay deterministic; the dedicated
    # login-OTP tests opt in by setting this True on their app instance.
    LOGIN_OTP_ENABLED = False


class ProductionConfig(Config):
    """Production configuration. HTTPS is assumed and enforced."""

    DEBUG = False
    SESSION_COOKIE_SECURE = True
    REMEMBER_COOKIE_SECURE = True


# Lookup table used by the application factory to select a configuration.
config = {
    "development": DevelopmentConfig,
    "testing": TestingConfig,
    "production": ProductionConfig,
    "default": DevelopmentConfig,
}
