"""هدرهای کش و فشرده‌سازی پاسخ — بیشترین اثر را روی سرعت واقعی سایت دارند.

مشکلی که حل می‌کند (اندازه‌گیری‌شده روی هاست):
1. فایل‌های استاتیک با `Cache-Control: no-cache` می‌رفتند، یعنی مرورگر برای هر بار
   عوض‌کردن صفحه باید تک‌تک ۱۹ فایل CSS/JS/فونت/آیکون را دوباره از سرور می‌پرسید.
   روی اتصالی با ۲۵۰ میلی‌ثانیه تأخیر، همین یک مورد حدود ۱.۴ ثانیه به هر کلیک اضافه می‌کرد.
2. هاست هیچ فشرده‌سازی‌ای نداشت؛ صفحه‌ی اصلی ۱۷۶ کیلوبایت خام بود که با gzip ۹ کیلوبایت می‌شود.

نکته‌ی مهم درباره‌ی امنیت این کار: کش طولانی فقط روی endpoint `static` اعمال می‌شود، نه روی
صفحه‌ها، نه روی خروجی‌های اکسل/PDF و نه روی عکس مشتری‌ها — پس هیچ داده‌ی زنده‌ای کهنه نمی‌شود.
"""
import gzip
import os

from flask import abort, request

VERSIONED_MAX_AGE = 31536000  # یک سال، برای آدرس‌هایی که ?v= دارند (با تغییر فایل، آدرس عوض می‌شود)
PLAIN_MAX_AGE = 604800        # یک هفته، برای آدرس بدون نسخه (مثل فونت‌ها که از داخل CSS صدا زده می‌شوند)

# چند فایل کوچک CSS/JS در یک آدرس سرو می‌شوند: روی HTTP/1.1 هر فایل یک رفت‌وبرگشت جداست
# و هاست هر کدام را هم از داخل پایتون جواب می‌دهد. ترتیب همان ترتیب قبلی در base.html است.
BUNDLES = {
    "app.css": (["css/variables.css", "css/base.css", "css/layout.css", "css/jalali-datepicker.css"],
                "text/css", "\n"),
    "app.js": (["js/theme.js", "js/toast.js", "js/tag-picker.js", "js/view-toggle.js",
                "js/jalali-datepicker.js", "js/pwa.js", "js/confirm-submit.js"],
               "text/javascript", "\n;\n"),
}

COMPRESS_MIN_BYTES = 1024
FILE_COMPRESS_MAX_BYTES = 1024 * 1024  # سقف خواندن فایل در حافظه برای فشرده‌سازی (CSS/JS ما زیر ۲۰ کیلوبایت‌اند)
COMPRESSIBLE_TYPES = {
    "text/html", "text/css", "text/plain", "text/xml", "text/csv",
    # نام نوعِ جاوااسکریپت بین نسخه‌های پایتون فرق می‌کند؛ هر سه شکلش اینجا هست
    "text/javascript", "application/javascript", "application/x-javascript",
    "application/json", "application/manifest+json",
    "image/svg+xml",
}


def register(app):
    static_folder = app.static_folder or ""
    versions = {}
    built = {}

    def _bundle_parts(name):
        parts, mimetype, separator = BUNDLES[name]
        return [os.path.join(static_folder, part.replace("/", os.sep)) for part in parts], mimetype, separator

    def bundle_version(name):
        paths, _mimetype, _separator = _bundle_parts(name)
        try:
            return str(int(max(os.stat(path).st_mtime for path in paths)))
        except OSError:
            return "0"

    def build_bundle(name):
        paths, mimetype, separator = _bundle_parts(name)
        body = separator.join(open(path, encoding="utf-8").read() for path in paths)
        return body, mimetype

    @app.route("/static/bundle/<name>")
    def static_bundle(name):
        if name not in BUNDLES:
            abort(404)
        if app.debug or name not in built:
            built[name] = build_bundle(name)  # در حالت توسعه هر بار از نو، تا ویرایش CSS فوری دیده شود
        body, mimetype = built[name]
        response = app.response_class(body, mimetype=mimetype)
        response.headers["Cache-Control"] = f"public, max-age={VERSIONED_MAX_AGE}, immutable"
        return response

    @app.url_defaults
    def add_bundle_version(endpoint, values):
        if endpoint == "static_bundle" and values.get("name") in BUNDLES and "v" not in values:
            values["v"] = bundle_version(values["name"])

    @app.url_defaults
    def add_static_version(endpoint, values):
        """به آدرس هر فایل استاتیک یک `?v=<زمان تغییر فایل>` اضافه می‌کند.

        بدون این، کشِ یک‌ساله یعنی آپدیت بعدیِ CSS/JS هرگز به کاربرانی که سایت را باز کرده‌اند نمی‌رسد.
        با این کار، به‌محض آپلود فایل جدید آدرسش عوض می‌شود و مرورگر نسخه‌ی تازه را می‌گیرد.
        """
        if endpoint != "static":
            return
        filename = values.get("filename")
        if not filename or "v" in values:
            return
        if filename.startswith("fonts/"):
            # فونت‌ها از داخل base.css با آدرس ثابت صدا زده می‌شوند؛ اگر اینجا ?v= بخورند
            # آدرسِ preload با آدرسِ CSS فرق می‌کند و مرورگر فونت را دو بار دانلود می‌کند.
            return
        version = versions.get(filename)
        if version is None:
            try:
                version = str(int(os.stat(os.path.join(static_folder, filename)).st_mtime))
            except OSError:
                version = ""  # فایل نیست؛ بی‌خیالِ نسخه (خود درخواست ۴۰۴ می‌گیرد)
            versions[filename] = version
        if version:
            values["v"] = version

    @app.after_request
    def cache_static_files(response):
        if request.endpoint == "static" and response.status_code in (200, 304):
            max_age = VERSIONED_MAX_AGE if request.args.get("v") else PLAIN_MAX_AGE
            immutable = ", immutable" if request.args.get("v") else ""
            response.headers["Cache-Control"] = f"public, max-age={max_age}{immutable}"
        return response

    @app.after_request
    def compress_response(response):
        """پاسخ‌های متنی را gzip می‌کند (هاست خودش این کار را نمی‌کند).

        فقط نوع‌های متنیِ لیست بالا فشرده می‌شوند؛ خروجی اکسل، PDF، بک‌آپ دیتابیس،
        عکس مشتری و فونت‌ها اصلاً دست نمی‌خورند (هم از قبل فشرده‌اند، هم نباید خراب شوند).
        """
        if response.status_code != 200 or "Content-Encoding" in response.headers:
            return response
        if (response.mimetype or "").lower() not in COMPRESSIBLE_TYPES:
            return response
        if "gzip" not in (request.headers.get("Accept-Encoding") or "").lower():
            return response
        if response.direct_passthrough or response.is_streamed:
            # فایل CSS/JS روی دیسک: فقط اگر کوچک باشد در حافظه خوانده می‌شود.
            # ETag دست نمی‌خورد، پس درخواست شرطی همچنان ۳۰۴ می‌گیرد (۳۰۴ اصلاً بدنه ندارد).
            length = response.content_length
            if not length or length > FILE_COMPRESS_MAX_BYTES:
                return response
            response.direct_passthrough = False

        data = response.get_data()
        if len(data) < COMPRESS_MIN_BYTES:
            return response  # برای چند صد بایت، فشرده‌سازی ارزشش را ندارد

        response.set_data(gzip.compress(data, 6))
        response.headers["Content-Encoding"] = "gzip"
        response.headers["Content-Length"] = str(response.calculate_content_length() or len(response.get_data()))
        response.vary.add("Accept-Encoding")  # تا پراکسی‌ها نسخه‌ی فشرده را به مرورگر بی‌پشتیبان ندهند
        return response
