import re

import mimetypes

from pyramid.view import view_config

from . import (
    get_referer_or_home,
    user_required,
    shop_owner_required,
    shop_editor_required,
)

from ..models.shop import (
    Shop,
    get_shop_by_id,
    get_shop_by_domain_name,
    is_shop_name_valid,
    is_shop_name_available,
)

from ..models.product import (
    get_products_from_a_shop,
    get_all_products_from_a_shop,
    get_products_by_keywords,
)

from ..models.user import (
    get_user_by_id,
    get_or_create_user_by_email,
)

from ..models.shop_search_request import (
    ShopSearchRequest,
)

from ..lib.mail import send_invite_email

from ..lib.phone_numbers import is_phone_number_valid

from ..lib.currency import dollars_to_cents, cents_to_dollars

from pyramid.httpexceptions import HTTPFound, HTTPNotFound
from pyramid.response import Response

# feel free to come up with a better plan, GPT-4 made this regex.
DOMAIN_NAME_REGEX = re.compile(
    r"^(?=.{1,253}$)(?:(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,9}$"
)


def checkbox_to_bool(checkbox):
    return checkbox == "on"


def bool_to_checkbox(boolean):
    return "on" if boolean else "off"


def _torrent_backfill_async(request, shop):
    """Fire-and-forget: generate .torrent bundles for all opted-in public products
    in a shop that have no magnet link yet.  Returns immediately — work runs in
    a daemon thread so the request is not blocked.

    Bundle rules:
      - paid products (is_sellable):  needs a preview file  — product file never seeded
      - free content  (!is_sellable): needs a content file  — fully distributed
    """
    import threading
    import logging
    from ..lib.torrent import generate_torrent, build_bundle_files, _capture_s3_creds, _make_client
    from ..models.product import Product

    log = logging.getLogger(__name__)

    # Snapshot everything needed before the request context is torn down.
    shop_id = shop.id
    cdn_endpoint = request.shop_cdn_endpoint
    bucket = request.shop_bucket_name
    creds = _capture_s3_creds(request.shop_uploads_client)
    session_factory = request.registry["dbsession_factory"]

    products = (
        request.dbsession.query(Product)
        .filter(Product.shop_id == shop_id)
        .all()
    )

    # Build work list now while the DB session is live.
    work = []
    for product in products:
        if product.torrent_magnet_link:
            continue
        if not product.torrent_opt_in or product.visibility != 1:
            continue
        if product.is_sellable:
            if "preview" not in product.originals:
                continue
        else:
            if "product" not in product.originals:
                continue
        bundle_files = build_bundle_files(product, cdn_endpoint)
        if not bundle_files:
            continue
        work.append({
            "bundle_name": product.slug or str(product.id),
            "files": bundle_files,
            "description": product.description or "",
            "s3_path": product.s3_path,
            "product_id": product.id,
        })

    if not work:
        log.info("torrent backfill: nothing to do for shop=%s", shop_id)
        return

    def _run():
        s3_client = _make_client(creds)
        log.info("torrent backfill: starting %d product(s) for shop=%s", len(work), shop_id)
        for item in work:
            try:
                generate_torrent(
                    s3_client, bucket,
                    bundle_name=item["bundle_name"],
                    files=item["files"],
                    description=item["description"],
                    s3_path=item["s3_path"],
                    product_id=item["product_id"],
                    session_factory=session_factory,
                    cdn_endpoint=cdn_endpoint,
                )
            except Exception:
                log.exception("torrent backfill: failed product=%s", item["product_id"])
        log.info("torrent backfill: finished for shop=%s", shop_id)

    t = threading.Thread(target=_run, daemon=True, name=f"torrent-backfill-{shop_id}")
    t.start()
    log.info("torrent backfill: background thread started for shop=%s, %d product(s)", shop_id, len(work))


def _checksum_backfill_async(request, shop):
    """Fire-and-forget: enumerate all product files needing checksums and
    compute them in the background.  Returns immediately — the entire loop
    runs in a daemon thread so the request is not blocked."""
    import threading
    import logging
    from ..lib.checksums import compute_checksums
    from ..lib.torrent import _capture_s3_creds, _make_client
    from ..models.product import Product

    log = logging.getLogger(__name__)

    all_file_keys = ["product", "preview", "thumbnail1", "thumbnail2", "thumbnail3", "thumbnail4"]

    # Snapshot everything we need before the request context is torn down.
    shop_id = shop.id
    bucket = request.shop_bucket_name
    creds = _capture_s3_creds(request.shop_uploads_client)
    session_factory = request.registry["dbsession_factory"]

    # Build the work list now (while the DB session is live).
    products = (
        request.dbsession.query(Product)
        .filter(Product.shop_id == shop_id)
        .all()
    )
    work = []
    for product in products:
        for file_key in all_file_keys:
            if file_key not in product.originals:
                continue
            existing = product.checksums.get(file_key, {})
            if existing.get("md5") and existing.get("sha256"):
                continue  # already done
            work.append((f"{product.s3_path}/{file_key}", file_key, product.id))

    if not work:
        log.info("checksum backfill: nothing to do for shop=%s", shop_id)
        return

    def _run():
        s3_client = _make_client(creds)
        log.info("checksum backfill: starting %d file(s) for shop=%s", len(work), shop_id)
        for s3_key, file_key, product_id in work:
            try:
                compute_checksums(s3_client, bucket, s3_key, file_key, product_id, session_factory)
            except Exception:
                log.exception("checksum backfill: failed s3_key=%s", s3_key)
        log.info("checksum backfill: finished for shop=%s", shop_id)

    t = threading.Thread(target=_run, daemon=True, name=f"checksum-backfill-{shop_id}")
    t.start()
    log.info("checksum backfill: background thread started for shop=%s, %d file(s)", shop_id, len(work))


def get_shop_from_matchdict(request, prefetched_shop=None):
    """
    This function uses the shop_id from the url path
    and returns a Shop object from the database or None.

    This protects the database from querying the same shop object twice,
    if the optional prefetched_shop Shop object has the same id as the
    matchdict in the route's path.
    """
    if prefetched_shop and str(prefetched_shop.id) == request.matchdict["shop_id"]:
        return prefetched_shop
    else:
        # returns None if shop_id is an invalid uuid.
        return get_shop_by_id(request.dbsession, request.matchdict["shop_id"])


@view_config(route_name="home", renderer="home.j2")
def home(request):
    if request.is_saas_domain and request.user and request.user.authenticated:
        if not request.shop and not request.user.active_shop:
            if not request.session.peek_flash():
                return HTTPFound("/s/new")
    products = None
    if request.shop:
        products = get_products_from_a_shop(request.shop)
    return {
        "products": products,
    }


@view_config(route_name="shop", renderer="shop.j2")
@view_config(route_name="shop_slug", renderer="shop.j2")
def shop(request):
    shop = get_shop_from_matchdict(request)
    if "slug" not in request.matchdict:
        return HTTPFound(f"/s/{shop.id}/{shop.slug}")

    ctx = {"products": get_products_from_a_shop(shop)}

    if shop.public_sales_stats:
        from ..models.invoice import Invoice
        from sqlalchemy import func as sa_func

        invoices = shop.invoices
        ctx["sales_count"] = invoices.count()
        ctx["sales_revenue"] = invoices.with_entities(
            sa_func.coalesce(sa_func.sum(Invoice.total_in_cents), 0)
        ).scalar() / 100.0

    return ctx


@view_config(route_name="shop_products", renderer="shop_products.j2")
@shop_editor_required()
def shop_products(request):
    msg = None
    shop = get_shop_from_matchdict(request)

    if shop is None:
        msg = ("That shop_id does not exist.", "error")

    if msg:
        request.session.flash(msg)
        return HTTPFound(get_referer_or_home(request))

    return {
        "products": get_all_products_from_a_shop(shop),
    }


@view_config(route_name="shop_about", renderer="shop_about.j2")
@view_config(route_name="shop_about_slug", renderer="shop_about.j2")
def shop_about(request):
    shop = get_shop_from_matchdict(request)
    if "slug" not in request.matchdict:
        return HTTPFound(f"/s/{shop.id}/{shop.slug}/about")
    return {}


@view_config(route_name="search", renderer="home.j2")
def search(request):
    keywords = request.params.get("keywords", None)

    if keywords is None:
        return HTTPFound(get_referer_or_home(request))

    keyword_list = [k for k in keywords.split(" ") if k][:10]
    products = get_products_by_keywords(
        request.dbsession, keyword_list, request.shop
    )

    hit_count = len(products)

    # log search_request for later reporting.
    search_request = ShopSearchRequest(
        keywords,
        hit_count,
        shop=request.shop,
        user=request.user,
    )
    request.dbsession.add(search_request)
    request.dbsession.flush()

    if hit_count == 1:
        # redirect to only match, the title slugified node uri.
        return HTTPFound(products[0].absolute_url(request))

    return {
        "products": products,
        "keywords": keywords,
    }


@view_config(route_name="shop_new", renderer="shop_new.j2")
@user_required(
    flash_msg="To create a new shop, please verify your email address below.",
    flash_level="info",
    redirect_to_route_name="join-or-log-in",
)
def shop_new(request):
    name = request.params.get("name", "").strip()
    phone_number = request.params.get("phone_number", "").strip()
    billing_address = request.params.get("billing_address", "").strip()
    description = request.params.get("description", "").strip()

    if name and description and billing_address and phone_number:
        if not is_shop_name_valid(name):
            msg = (
                "Invalid shop name, only use alpha numeric, spaces, dashes, or periods.",
                "error",
            )
            request.session.flash(msg)
        elif not is_shop_name_available(request.dbsession, name):
            msg = ("That shop name is already in use. Please pick another.", "error")
            request.session.flash(msg)
        elif not is_phone_number_valid(phone_number):
            msg = ("Invalid phone number. We accept: -.() and numbers.", "error")
            request.session.flash(msg)

        else:
            # MPS-14: environment selector (default production)
            environment = int(request.params.get("environment", "0"))
            if environment not in (0, 1, 2):
                environment = 0

            # MPS-14: enforce 2 free dev/stage shops per production shop
            if environment != 0:
                prod_count = sum(1 for s in request.user.shops if s.is_production)
                non_prod_count = sum(1 for s in request.user.shops if s.is_non_production)
                allowed = prod_count * 2
                if non_prod_count >= allowed:
                    request.session.flash((
                        "You need a production shop before creating dev/stage shops. "
                        "Each production shop includes 2 free dev/stage shops.",
                        "error",
                    ))
                    return HTTPFound("/s/new")

            import time as _time
            shop = Shop(name, phone_number, billing_address, description)
            shop.environment = environment
            shop.trial_started_timestamp = int(_time.time() * 1000)
            shop.add_user_to_shop(request.user)
            request.user.set_active_shop(shop)
            request.dbsession.add(shop)
            request.dbsession.add(request.user)
            request.dbsession.flush()
            msg = (
                "Great work, you created a shop! You may continue to setup your shop or start posting products!",
                "success",
            )
            request.session.flash(msg)
            return HTTPFound(f"/s/{shop.id}/settings")

    elif name or description or billing_address or phone_number:
        msg = ("You must fill out all fields.", "error")
        request.session.flash(msg)

    return {
        "name": name,
        "description": description,
        "billing_address": billing_address,
        "phone_number": phone_number,
    }


@view_config(route_name="user_shop_activate")
@shop_editor_required()
def shop_activate(request):
    shop = get_shop_from_matchdict(request, prefetched_shop=request.user.active_shop)

    if shop is None:
        msg = ("That shop_id does not exist.", "error")

    elif shop == request.user.active_shop:
        msg = ("This shop is already your active shop.", "success")

    else:
        msg = ("You activated that shop.", "success")
        request.user.active_shop_id = shop.id
        request.dbsession.add(request.user)
        request.dbsession.flush()

    request.session.flash(msg)
    return HTTPFound("/")


@view_config(route_name="shop_terms_of_service", renderer="markup_renderer.j2")
@view_config(route_name="shop_terms_of_service2", renderer="markup_renderer.j2")
def shop_terms_of_service(request):
    return {
        "markup_rendered": request.shop.terms_of_service_html,
        "markup_path": "terms",
    }


@view_config(route_name="shop_privacy_policy", renderer="markup_renderer.j2")
@view_config(route_name="shop_privacy_policy2", renderer="markup_renderer.j2")
def shop_privacy_policy(request):
    return {
        "markup_rendered": request.shop.privacy_policy_html,
        "markup_path": "privacy-policy",
    }


@view_config(route_name="shop_edit_terms_of_service", renderer="markup_editor.j2")
@shop_owner_required()
def shop_edit_terms_of_service(request):
    shop = request.user.active_shop
    if request.shop:
        shop = request.shop

    raw_markup_data = request.params.get(
        "markup-editor-textarea", shop.terms_of_service_raw
    )

    if raw_markup_data and raw_markup_data != shop.terms_of_service_raw:
        shop.terms_of_service = raw_markup_data
        msg = ("You set the shop's Terms of Service!", "success")
        request.session.flash(msg)
        request.dbsession.add(shop)
        request.dbsession.flush()

    return {
        "markup_subject": "Terms of Service",
        "markup_rendered": shop.terms_of_service_html,
        "markup_raw": shop.terms_of_service_raw,
        "markup_form_path": shop.absolute_terms_url(request),
    }


@view_config(route_name="shop_edit_privacy_policy", renderer="markup_editor.j2")
@shop_owner_required()
def shop_edit_privacy_policy(request):
    shop = request.user.active_shop
    if request.shop:
        shop = request.shop

    raw_markup_data = request.params.get(
        "markup-editor-textarea", shop.privacy_policy_raw
    )

    if raw_markup_data and raw_markup_data != shop.privacy_policy_raw:
        shop.privacy_policy = raw_markup_data
        msg = ("You set the shop's Privacy Policy!", "success")
        request.session.flash(msg)
        request.dbsession.add(shop)
        request.dbsession.flush()

    return {
        "markup_subject": "Privacy Policy",
        "markup_rendered": shop.privacy_policy_html,
        "markup_raw": shop.privacy_policy_raw,
        "markup_form_path": shop.absolute_privacy_policy_url(request),
    }


@view_config(route_name="shop_users", renderer="shop_users.j2")
@shop_owner_required()
def shop_users(request):
    shop = request.shop
    email_to_invite = request.params.get("email-to-invite", "")
    role_id = request.params.get("role-id")

    _email_regex = re.compile(r"^[^@]+@[^@]+\.[^.@]+$")

    if "submit" in request.params:
        if not email_to_invite:
            request.session.flash(("Please choose an email to invite.", "error"))
        elif not role_id:
            request.session.flash(("Please choose a role.", "error"))
        elif email_to_invite and _email_regex.match(email_to_invite) is None:
            # posted email does not pass regex the match object is None.
            request.session.flash(("That email address is invalid.", "error"))
        elif email_to_invite and role_id:
            # get or create new user if it doesn't exist.
            user = get_or_create_user_by_email(request.dbsession, email_to_invite)

            # make new user a shop user of the proper role.
            request.shop.add_user_to_shop(user, role_id)

            # commit changes to database.
            request.dbsession.add(user)
            request.dbsession.flush()

            # send an invitation email to the new user.
            send_invite_email(request, email_to_invite, request.user, request.shop)

            msg = (f"You invited {email_to_invite} to the shop.", "success")
            request.session.flash(msg)

    return {
        "email": email_to_invite or "",
        "owners": list(set(shop.owners)),
        "editors": list(set(shop.editors)),
        "members": list(set(shop.members)),
    }


@view_config(route_name="shop_user_remove")
@shop_owner_required()
def shop_user_remove(request):
    user_id = request.params.get("user_id", None)
    if user_id:
        user = get_user_by_id(request.dbsession, user_id)
        if user:
            if user == request.user:
                msg = (
                    f"Sheepishly refusing to remove myself! {user.email}",
                    "error",
                )
                request.session.flash(msg)
            else:
                user_shop = request.shop.get_usershop_for_user(user)
                request.dbsession.delete(user_shop)
                msg = (f"You removed {user.email} from the shop.", "success")
                request.session.flash(msg)
    return HTTPFound(f"/s/{request.shop.id}/users")


@view_config(route_name="shop_settings", renderer="shop_settings.j2")
@shop_owner_required()
def shop_settings(request):
    shop = request.shop
    name = request.params.get("name", shop.name).strip()
    phone_number = request.params.get("phone_number", shop.phone_number).strip()
    billing_address = request.params.get(
        "billing_address", shop.billing_address
    ).strip()
    description = request.params.get("description", shop.description).strip()
    domain_name = (
        request.params.get("domain_name", shop.domain_name or "").strip().lower()
    )

    # announcement ribbon settings.
    ribbon_text = request.params.get("ribbon_text", shop.ribbon_text).strip()
    ribbon_text_color = request.params.get(
        "ribbon_text_color", shop.ribbon_text_color
    ).strip()
    ribbon_color_1 = request.params.get("ribbon_color_1", shop.ribbon_color_1).strip()
    ribbon_color_2 = request.params.get("ribbon_color_2", shop.ribbon_color_2).strip()

    # Theme settings
    default_theme = request.params.get("default_theme", shop.default_theme)

    # Grid lanes setting (keep current value by default)
    grid_lanes_enabled = shop.grid_lanes_enabled

    google_analytics_id = request.params.get(
        "google_analytics_id", shop.google_analytics_id
    )
    plausible_domain_name = request.params.get(
        "plausible_domain_name", shop.plausible_domain_name or ""
    ).strip()

    google_site_verification = request.params.get(
        "google_site_verification", shop.google_site_verification or ""
    ).strip()

    stripe_test_mode = request.app.get("stripe.test_mode", False)
    stripe_public_api_key = request.params.get(
        "stripe_public_api_key", shop.stripe_public_api_key
    )
    stripe_secret_api_key = request.params.get(
        "stripe_secret_api_key", shop.stripe_secret_api_key
    )

    # maint_mode_checkbox = request.params.get("maint-mode-checkbox", bool_to_checkbox(shop.maint_mode))
    maint_mode_checkbox = request.params.get("maint-mode-checkbox", "off")
    maint_mode = checkbox_to_bool(maint_mode_checkbox)

    # Determine which form was submitted
    form_section = request.params.get("form_section", "")

    # Comment system settings
    comments_enabled = shop.comments_enabled  # Keep current value by default
    comments_require_purchase = shop.comments_require_purchase
    comments_require_approval = shop.comments_require_approval

    # Only update comment settings if the comment form was submitted
    if form_section == "comment-settings":
        comments_enabled_checkbox = request.params.get(
            "comments-enabled-checkbox", "off"
        )
        comments_enabled = checkbox_to_bool(comments_enabled_checkbox)
        comments_require_purchase_checkbox = request.params.get(
            "comments-require-purchase-checkbox", "off"
        )
        comments_require_purchase = checkbox_to_bool(comments_require_purchase_checkbox)
        comments_require_approval_checkbox = request.params.get(
            "comments-require-approval-checkbox", "off"
        )
        comments_require_approval = checkbox_to_bool(comments_require_approval_checkbox)

    # Crypto settings
    crypto_quote_expiry_seconds = request.params.get(
        "crypto_quote_expiry_seconds", shop.crypto_quote_expiry_seconds
    )
    # Get dollar values from form
    payment_risk_threshold_mid_dollars = request.params.get(
        "payment_risk_threshold_mid_dollars"
    )
    payment_risk_threshold_high_dollars = request.params.get(
        "payment_risk_threshold_high_dollars"
    )

    s3_webhook_key = request.params.get("key")
    s3_webhook_bucket = request.params.get("bucket")
    s3_webhook_etag = request.params.get("etag")

    uploaded_file_key = request.params.get("uploaded", "")

    if uploaded_file_key:
        request.session.flash(
            (
                f"You successfully uploaded a new {uploaded_file_key} file!",
                "success",
            )
        )

    if request.method == "POST":
        # MPS-15: Block settings changes when trial expired, except
        # environment-settings and bucket-settings (needed for onboarding)
        allowed_when_expired = ("environment-settings", "bucket-settings")
        if shop.is_trial_expired and form_section not in allowed_when_expired:
            request.session.flash((
                "Your 21-day trial has expired. Choose a plan to continue editing settings.",
                "error",
            ))
            return HTTPFound(f"/s/{shop.id}/settings")

        # Handle shop settings form
        if form_section == "shop-settings":
            if name != shop.name:
                if not is_shop_name_valid(name):
                    msg = (
                        "Invalid shop name, only use alpha numeric, spaces, dashes, or periods.",
                        "error",
                    )
                elif not is_shop_name_available(request.dbsession, name):
                    msg = (
                        "That shop name is already in use. Please pick another.",
                        "error",
                    )
                else:
                    shop.name = name
                    msg = ("You set the shop's name.", "success")
                request.session.flash(msg)

            if phone_number != shop.phone_number:
                if is_phone_number_valid(phone_number):
                    shop.phone_number = phone_number
                    msg = ("You set the shop's phone number.", "success")
                else:
                    msg = (
                        "Invalid phone number. We accept: -.() and numbers.",
                        "error",
                    )
                request.session.flash(msg)

            if billing_address and billing_address != shop.billing_address:
                shop.billing_address = billing_address
                msg = ("You set the shop's billing address.", "success")
                request.session.flash(msg)

            if description and description != shop.description:
                shop.description = description
                msg = ("You set the shop's description.", "success")
                request.session.flash(msg)

        # Handle integration settings form
        if form_section == "integration-settings":
            if domain_name != shop.domain_name:
                if not domain_name:
                    # clear the domain name.
                    shop.domain_name = domain_name
                    msg = ("You cleared the shop's domain name.", "success")
                elif DOMAIN_NAME_REGEX.match(domain_name):
                    existing_shop = get_shop_by_domain_name(
                        request.dbsession, domain_name
                    )
                    if existing_shop:
                        msg = (
                            "That domain name is already in use. Please pick another.",
                            "error",
                        )
                    else:
                        shop.domain_name = domain_name
                        msg = ("You set the shop's domain name.", "success")
                else:
                    msg = ("Invalid domain name. Please enter a valid domain.", "error")
                request.session.flash(msg)

            if google_analytics_id != (shop.google_analytics_id or ""):
                shop.google_analytics_id = google_analytics_id
                msg = ("You set the shop's Google Analytics Id.", "success")
                request.session.flash(msg)

            if plausible_domain_name != (shop.plausible_domain_name or ""):
                shop.plausible_domain_name = plausible_domain_name
                msg = ("You set the shop's Plausible Analytics Domain Name.", "success")
                request.session.flash(msg)

            if google_site_verification != (shop.google_site_verification or ""):
                shop.google_site_verification = google_site_verification
                if google_site_verification:
                    msg = ("You set the shop's Google Site Verification code.", "success")
                else:
                    msg = ("You cleared the shop's Google Site Verification code.", "success")
                request.session.flash(msg)

        # Handle ribbon settings form
        if form_section == "ribbon-settings":
            if ribbon_text != (shop.ribbon_text or ""):
                shop.ribbon_text = ribbon_text
                msg = ("You set the shop's announcement ribbon text.", "success")
                request.session.flash(msg)

            if ribbon_text_color != (shop.ribbon_text_color or ""):
                shop.ribbon_text_color = ribbon_text_color
                msg = ("You set the shop's announcement ribbon text color.", "success")
                request.session.flash(msg)

            if ribbon_color_1 != (shop.ribbon_color_1 or ""):
                shop.ribbon_color_1 = ribbon_color_1
                msg = (
                    "You set the shop's announcement ribbon background color 1.",
                    "success",
                )
                request.session.flash(msg)

            if ribbon_color_2 != (shop.ribbon_color_2 or ""):
                shop.ribbon_color_2 = ribbon_color_2
                msg = (
                    "You set the shop's announcement ribbon background color 2.",
                    "success",
                )
                request.session.flash(msg)

            # Handle default theme setting
            if default_theme and int(default_theme) != shop.default_theme:
                shop.default_theme = int(default_theme)
                theme_name = "Dark Mode" if int(default_theme) == 0 else "Light Mode"
                msg = (
                    f"Shop default theme set to {theme_name}.",
                    "success",
                )
                request.session.flash(msg)

            # Handle grid lanes setting
            grid_lanes_checkbox = request.params.get("grid-lanes-checkbox", "off")
            grid_lanes_enabled = checkbox_to_bool(grid_lanes_checkbox)
            if shop.grid_lanes_enabled != grid_lanes_enabled:
                shop.grid_lanes_enabled = grid_lanes_enabled
                status = "enabled" if grid_lanes_enabled else "disabled"
                request.session.flash(
                    (
                        f"Masonry layout (Grid Lanes) {status}",
                        "success",
                    )
                )

            # Handle show dates setting
            show_dates_value = request.params.get("show_dates")
            if show_dates_value is not None:
                show_dates = int(show_dates_value) == 1
                if shop.show_dates != show_dates:
                    shop.show_dates = show_dates
                    status = "visible" if show_dates else "hidden"
                    request.session.flash(
                        (
                            f"Product and content dates are now {status}",
                            "success",
                        )
                    )

            # Handle watch mode setting
            watch_mode_value = request.params.get("watch_mode")
            if watch_mode_value is not None:
                watch_mode = int(watch_mode_value) == 1
                if shop.watch_mode_enabled != watch_mode:
                    shop.watch_mode_enabled = watch_mode
                    status = "enabled" if watch_mode else "disabled"
                    request.session.flash(
                        (
                            f"Watch mode is now {status}",
                            "success",
                        )
                    )

            # Handle color filter setting
            color_filter_value = request.params.get("color_filter")
            if color_filter_value is not None:
                color_filter = int(color_filter_value)
                if color_filter not in (0, 1, 2, 3, 4, 5, 6, 7):
                    color_filter = 0
                if shop.color_filter != color_filter:
                    shop.color_filter = color_filter
                    filter_names = {
                        0: "Off",
                        1: "Grayscale",
                        2: "Red only",
                        3: "Green only",
                        4: "Blue only",
                        5: "Red + Green",
                        6: "Red + Blue",
                        7: "Green + Blue",
                    }
                    request.session.flash(
                        (
                            f"Color filter set to {filter_names[color_filter]}",
                            "success",
                        )
                    )

            # Handle sandbox mode setting
            sandbox_mode_value = request.params.get("sandbox_mode")
            if sandbox_mode_value is not None:
                sandbox_mode_enabled = int(sandbox_mode_value) == 1
                if shop.sandbox_mode != sandbox_mode_enabled:
                    shop.sandbox_mode = sandbox_mode_enabled
                    status = "enabled" if sandbox_mode_enabled else "disabled"
                    request.session.flash(
                        (
                            f"Sandbox mode is now {status}",
                            "success",
                        )
                    )

            # Handle public sales stats setting
            public_sales_stats_value = request.params.get("public_sales_stats")
            if public_sales_stats_value is not None:
                public_sales_stats = int(public_sales_stats_value) == 1
                if shop.public_sales_stats != public_sales_stats:
                    shop.public_sales_stats = public_sales_stats
                    status = "enabled" if public_sales_stats else "disabled"
                    request.session.flash(
                        (
                            f"Public sales stats are now {status}",
                            "success",
                        )
                    )

        # Handle stripe settings form
        if form_section == "stripe-settings":
            # Handle disable action
            if request.params.get("disable_stripe"):
                shop.stripe_enabled = False
                request.session.flash(("Stripe payments disabled", "success"))

            # Handle re-enable action - if stripe is disabled and we're submitting stripe settings
            elif not shop.stripe_enabled:
                shop.stripe_enabled = True
                request.session.flash(("Stripe payments re-enabled", "success"))

            elif stripe_public_api_key != shop.stripe_public_api_key:
                if stripe_public_api_key.startswith("pk_"):
                    if not stripe_test_mode and "_test_" in stripe_public_api_key:
                        msg = (
                            "Test Stripe keys are not allowed. Please create a test shop at test.makepostsell.com.",
                            "error",
                        )
                    else:
                        shop.stripe_public_api_key = stripe_public_api_key
                        # Re-enable Stripe if it was disabled and we're setting valid keys
                        if not shop.stripe_enabled:
                            shop.stripe_enabled = True
                            msg = (
                                "Stripe payments re-enabled and public key updated.",
                                "success",
                            )
                        else:
                            msg = (
                                "You set the shop's stripe_public_api_key.",
                                "success",
                            )
                else:
                    msg = (
                        "The shop's stripe_public_api_key must start with 'pk_'.",
                        "error",
                    )
                request.session.flash(msg)

            if stripe_secret_api_key != shop.stripe_secret_api_key:
                if stripe_secret_api_key.startswith("sk_"):
                    if not stripe_test_mode and "_test_" in stripe_secret_api_key:
                        msg = (
                            "Test Stripe keys are not allowed. Please create a test shop at test.makepostsell.com.",
                            "error",
                        )
                    else:
                        shop.stripe_secret_api_key = stripe_secret_api_key
                        # Re-enable Stripe if it was disabled and we're setting valid keys
                        if not shop.stripe_enabled:
                            shop.stripe_enabled = True
                            msg = (
                                "Stripe payments re-enabled and secret key updated.",
                                "success",
                            )
                        else:
                            msg = (
                                "You set the shop's stripe_secret_api_key.",
                                "success",
                            )
                else:
                    msg = (
                        "The shop's stripe_secret_api_key must start with 'sk_'.",
                        "error",
                    )
                request.session.flash(msg)

        # Handle PayPal settings form
        if form_section == "paypal-settings":
            paypal_client_id = request.params.get("paypal_client_id", "").strip()
            paypal_secret = request.params.get("paypal_secret", "").strip()

            # Handle disable action
            if request.params.get("disable_paypal"):
                shop.paypal_enabled = False
                request.session.flash(("PayPal payments disabled", "success"))
            # Handle re-enable action
            elif not shop.paypal_enabled:
                shop.paypal_enabled = True
                request.session.flash(("PayPal payments re-enabled", "success"))
            else:
                if paypal_client_id and paypal_client_id != shop.paypal_client_id:
                    shop.paypal_client_id = paypal_client_id
                    request.session.flash(("You set the shop's PayPal Client ID.", "success"))

                if paypal_secret and paypal_secret != shop.paypal_secret:
                    shop.paypal_secret = paypal_secret
                    request.session.flash(("You set the shop's PayPal Secret.", "success"))

        # Handle Adyen settings form
        if form_section == "adyen-settings":
            adyen_api_key = request.params.get("adyen_api_key", "").strip()
            adyen_merchant_account = request.params.get("adyen_merchant_account", "").strip()
            adyen_client_key = request.params.get("adyen_client_key", "").strip()
            adyen_hmac_key = request.params.get("adyen_hmac_key", "").strip()

            # Handle disable action
            if request.params.get("disable_adyen"):
                shop.adyen_enabled = False
                request.session.flash(("Adyen payments disabled", "success"))
            # Handle re-enable action
            elif not shop.adyen_enabled:
                shop.adyen_enabled = True
                request.session.flash(("Adyen payments re-enabled", "success"))
            else:
                if adyen_api_key and adyen_api_key != shop.adyen_api_key:
                    shop.adyen_api_key = adyen_api_key
                    request.session.flash(("You set the shop's Adyen API Key.", "success"))

                if adyen_merchant_account and adyen_merchant_account != shop.adyen_merchant_account:
                    shop.adyen_merchant_account = adyen_merchant_account
                    request.session.flash(("You set the shop's Adyen Merchant Account.", "success"))

                if adyen_client_key and adyen_client_key != shop.adyen_client_key:
                    shop.adyen_client_key = adyen_client_key
                    request.session.flash(("You set the shop's Adyen Client Key.", "success"))

                if adyen_hmac_key and adyen_hmac_key != shop.adyen_hmac_key:
                    shop.adyen_hmac_key = adyen_hmac_key
                    request.session.flash(("You set the shop's Adyen HMAC Key.", "success"))

        # Handle unsandbox settings form (MPS-22: gated by karaoke kill switch)
        if form_section == "unsandbox-settings":
            if not request.karaoke_enabled:
                raise HTTPNotFound()
            unsandbox_public_key = request.params.get("unsandbox_public_key", "").strip()
            unsandbox_secret_key = request.params.get("unsandbox_secret_key", "").strip()
            keys_changed = False

            if unsandbox_public_key and unsandbox_public_key != (shop.unsandbox_public_key or ""):
                if unsandbox_public_key.startswith("unsb-pk-"):
                    shop.unsandbox_public_key = unsandbox_public_key
                    keys_changed = True
                    request.session.flash(("Unsandbox public key saved.", "success"))
                else:
                    request.session.flash(("Unsandbox public key must start with 'unsb-pk-'.", "error"))

            if unsandbox_secret_key and unsandbox_secret_key != (shop.unsandbox_secret_key or ""):
                if unsandbox_secret_key.startswith("unsb-sk-"):
                    shop.unsandbox_secret_key = unsandbox_secret_key
                    keys_changed = True
                    request.session.flash(("Unsandbox secret key saved.", "success"))
                else:
                    request.session.flash(("Unsandbox secret key must start with 'unsb-sk-'.", "error"))

            # Validate keys against unsandbox API and backfill if valid
            if keys_changed and shop.unsandbox_public_key and shop.unsandbox_secret_key:
                from ..lib.un import validate_keys
                try:
                    validate_keys(shop.unsandbox_public_key, shop.unsandbox_secret_key)
                except Exception as e:
                    request.session.flash((f"Unsandbox API keys are invalid or expired: {e}. Keys saved — fix or extend at unsandbox.com.", "error"))
                else:
                    from ..lib.karaoke import backfill_karaoke_async
                    backfill_karaoke_async(
                        shop.id,
                        request.registry["dbsession_factory"],
                        request.app,
                    )
                    request.session.flash(("Backfilling vocal isolation for existing catalog in background.", "success"))

        # Handle backfill karaoke button (MPS-22: gated by karaoke kill switch)
        if form_section == "backfill-karaoke":
            if not request.karaoke_enabled:
                raise HTTPNotFound()
            if shop.unsandbox_public_key and shop.unsandbox_secret_key:
                from ..lib.karaoke import backfill_karaoke_async
                backfill_karaoke_async(
                    shop.id,
                    request.registry["dbsession_factory"],
                    request.app,
                )
                request.session.flash(("Backfilling vocal isolation for existing catalog in background.", "success"))
            else:
                request.session.flash(("Set unsandbox API keys first.", "error"))

        # Handle S3 mirror bucket settings
        if form_section == "mirror-settings":
            mirror_s3_endpoint = request.params.get("mirror_s3_endpoint", "").strip()
            mirror_s3_region = request.params.get("mirror_s3_region", "").strip()
            mirror_s3_bucket = request.params.get("mirror_s3_bucket", "").strip()
            mirror_s3_access_key = request.params.get("mirror_s3_access_key", "").strip()
            mirror_s3_secret_key = request.params.get("mirror_s3_secret_key", "").strip()
            mirror_s3_enabled = checkbox_to_bool(
                request.params.get("mirror_s3_enabled_checkbox", "off")
            )

            # If all credential fields are empty, clear everything
            if not any([mirror_s3_endpoint, mirror_s3_bucket, mirror_s3_access_key, mirror_s3_secret_key]):
                shop.mirror_s3_endpoint = None
                shop.mirror_s3_region = None
                shop.mirror_s3_bucket = None
                shop.mirror_s3_access_key = None
                shop.mirror_s3_secret_key = None
                shop.mirror_s3_enabled = False
                request.session.flash(("Mirror storage credentials cleared.", "success"))
            elif mirror_s3_endpoint and not mirror_s3_endpoint.startswith("http"):
                request.session.flash(("Mirror S3 endpoint must start with http:// or https://", "error"))
            elif not (mirror_s3_endpoint and mirror_s3_bucket and mirror_s3_access_key and mirror_s3_secret_key):
                request.session.flash(("Endpoint, bucket, access key, and secret key are all required.", "error"))
            else:
                shop.mirror_s3_endpoint = mirror_s3_endpoint
                shop.mirror_s3_region = mirror_s3_region or None
                shop.mirror_s3_bucket = mirror_s3_bucket
                shop.mirror_s3_access_key = mirror_s3_access_key
                shop.mirror_s3_secret_key = mirror_s3_secret_key
                shop.mirror_s3_enabled = mirror_s3_enabled

                # Test connection if enabled
                if mirror_s3_enabled:
                    from ..lib.s3_mirror import test_mirror_connection
                    ok, err = test_mirror_connection(shop)
                    if ok:
                        request.session.flash(("Mirror bucket configured and connection verified.", "success"))
                    else:
                        request.session.flash((f"Mirror credentials saved but connection test failed: {err}", "error"))
                else:
                    request.session.flash(("Mirror credentials saved. Enable the checkbox to activate sync.", "success"))

        # Handle backfill S3 mirror button
        if form_section == "backfill-s3-mirror":
            if shop.has_s3_mirror:
                from ..lib.s3_mirror import backfill_mirror_async
                backfill_mirror_async(
                    shop.id,
                    request.registry["dbsession_factory"],
                    request.app,
                )
                request.session.flash(("Mirroring existing files to your bucket in background.", "success"))
            else:
                request.session.flash(("Configure and enable mirror bucket credentials first.", "error"))

        # Handle maintenance settings form
        if form_section == "maintenance-settings":
            if shop.maint_mode != maint_mode:
                shop.maint_mode = maint_mode
                request.session.flash(
                    (
                        f"You turned {'on' if maint_mode else 'off'} maintenance mode",
                        "success",
                    )
                )

        # Handle subscription settings
        if form_section == "subscription-settings":
            subscriptions_enabled_checkbox = request.params.get(
                "subscriptions-enabled-checkbox", "off"
            )
            subscriptions_enabled = checkbox_to_bool(subscriptions_enabled_checkbox)
            if shop.subscriptions_enabled != subscriptions_enabled:
                shop.subscriptions_enabled = subscriptions_enabled
                status = "enabled" if subscriptions_enabled else "disabled"
                request.session.flash(
                    (
                        f"Email digest subscriptions {status}",
                        "success",
                    )
                )

        # Handle comment system settings only if comment form was submitted
        if form_section == "comment-settings":
            if shop.comments_enabled != comments_enabled:
                shop.comments_enabled = comments_enabled
                status = "enabled" if comments_enabled else "disabled"
                request.session.flash(
                    (
                        f"Comments {status} for this shop",
                        "success",
                    )
                )

            if shop.comments_require_purchase != comments_require_purchase:
                shop.comments_require_purchase = comments_require_purchase
                status = "enabled" if comments_require_purchase else "disabled"
                request.session.flash(
                    (
                        f"Purchase requirement for comments {status}",
                        "success",
                    )
                )

            if shop.comments_require_approval != comments_require_approval:
                shop.comments_require_approval = comments_require_approval
                status = "enabled" if comments_require_approval else "disabled"
                request.session.flash(
                    (
                        f"Comment approval requirement {status}",
                        "success",
                    )
                )

        # Handle crypto settings only if crypto form was submitted
        if form_section == "crypto-settings":
            try:
                seconds = int(crypto_quote_expiry_seconds)
                if seconds != shop.crypto_quote_expiry_seconds:
                    if 300 <= seconds <= 7200:
                        shop.crypto_quote_expiry_seconds = seconds
                        request.session.flash(
                            (
                                f"Cryptocurrency quote expiry set to {seconds} seconds",
                                "success",
                            )
                        )
                    else:
                        request.session.flash(
                            (
                                "Quote expiry must be between 300 and 7200 seconds",
                                "error",
                            )
                        )
            except (ValueError, TypeError):
                request.session.flash(("Invalid quote expiry time", "error"))

            # Handle payment risk thresholds
            if payment_risk_threshold_mid_dollars is not None:
                try:
                    dollars = float(payment_risk_threshold_mid_dollars)
                    cents = dollars_to_cents(dollars)
                    if 100 <= cents <= 100000:
                        if cents != shop.payment_risk_threshold_mid_cents:
                            shop.payment_risk_threshold_mid_cents = cents
                            request.session.flash(
                                (
                                    f"Medium risk threshold set to ${dollars:.2f}",
                                    "success",
                                )
                            )
                    else:
                        request.session.flash(
                            (
                                "Medium risk threshold must be between $1 and $1000",
                                "error",
                            )
                        )
                except (ValueError, TypeError):
                    request.session.flash(("Invalid medium risk threshold", "error"))

            if payment_risk_threshold_high_dollars is not None:
                try:
                    dollars = float(payment_risk_threshold_high_dollars)
                    cents = dollars_to_cents(dollars)
                    if 1000 <= cents <= 1000000:
                        if cents != shop.payment_risk_threshold_high_cents:
                            shop.payment_risk_threshold_high_cents = cents
                            request.session.flash(
                                (
                                    f"High risk threshold set to ${dollars:.2f}",
                                    "success",
                                )
                            )
                    else:
                        request.session.flash(
                            (
                                "High risk threshold must be between $10 and $10000",
                                "error",
                            )
                        )
                except (ValueError, TypeError):
                    request.session.flash(("Invalid high risk threshold", "error"))

        # Handle gift card settings
        if form_section == "gift-card-settings":
            gc_enabled_checkbox = request.params.get("gift-card-enabled-checkbox", "off")
            gc_enabled = checkbox_to_bool(gc_enabled_checkbox)
            if shop.gift_card_enabled != gc_enabled:
                shop.gift_card_enabled = gc_enabled
                status = "enabled" if gc_enabled else "disabled"
                request.session.flash((f"Gift cards {status}.", "success"))

            try:
                gc_min = float(request.params.get("gift_card_min", "5.00"))
                gc_max = float(request.params.get("gift_card_max", "250.00"))
                gc_min_cents = int(gc_min * 100)
                gc_max_cents = int(gc_max * 100)

                if gc_min_cents < 100:
                    request.session.flash(("Minimum gift card amount must be at least $1.00.", "error"))
                elif gc_max_cents < gc_min_cents:
                    request.session.flash(("Maximum must be greater than or equal to minimum.", "error"))
                elif gc_max_cents > 1000000:
                    request.session.flash(("Maximum gift card amount cannot exceed $10,000.", "error"))
                else:
                    if shop.gift_card_min_in_cents != gc_min_cents:
                        shop.gift_card_min_in_cents = gc_min_cents
                        request.session.flash((f"Gift card minimum set to ${gc_min:,.2f}.", "success"))
                    if shop.gift_card_max_in_cents != gc_max_cents:
                        shop.gift_card_max_in_cents = gc_max_cents
                        request.session.flash((f"Gift card maximum set to ${gc_max:,.2f}.", "success"))
            except (ValueError, TypeError):
                request.session.flash(("Invalid gift card amount.", "error"))

        # Handle environment settings (MPS-14)
        if form_section == "environment-settings":
            env_value = int(request.params.get("environment", "0"))
            if env_value not in (0, 1, 2):
                request.session.flash(("Invalid environment value.", "error"))
            elif env_value != 0 and shop.environment == 0:
                # Changing from production to non-production — check allowance
                # Count OTHER production shops (excluding this one being changed)
                prod_count = sum(1 for s in request.user.shops if s.is_production and s.id != shop.id)
                non_prod_count = sum(1 for s in request.user.shops if s.is_non_production)
                # Each production shop allows 2 non-prod shops; minimum 2 non-prod allowed
                max_non_prod = max(2, prod_count * 2)
                if non_prod_count >= max_non_prod:
                    request.session.flash((
                        "You need more production shops before creating additional dev/stage shops. "
                        "Each production shop includes 2 free dev/stage shops.",
                        "error",
                    ))
                else:
                    shop.environment = env_value
                    request.session.flash((
                        f"Shop environment changed to {shop.environment_label}. "
                        "This shop is now hidden from public search and feeds.",
                        "success",
                    ))
            elif env_value == 0 and shop.environment != 0:
                # Changing from non-production to production
                shop.environment = env_value
                request.session.flash((
                    "Shop environment changed to Production. "
                    "This shop is now publicly visible.",
                    "success",
                ))
            elif env_value != shop.environment:
                shop.environment = env_value
                request.session.flash((
                    f"Shop environment changed to {shop.environment_label}.",
                    "success",
                ))

        # Handle primary S3 bucket settings (MPS-16)
        if form_section == "bucket-settings":
            ps3_endpoint = request.params.get("primary_s3_endpoint", "").strip()
            ps3_region = request.params.get("primary_s3_region", "").strip()
            ps3_bucket = request.params.get("primary_s3_bucket", "").strip()
            ps3_access_key = request.params.get("primary_s3_access_key", "").strip()
            ps3_secret_key = request.params.get("primary_s3_secret_key", "").strip()
            ps3_cdn_endpoint = request.params.get("primary_s3_cdn_endpoint", "").strip()
            ps3_enabled = checkbox_to_bool(request.params.get("primary_s3_enabled_checkbox", "off"))

            if ps3_enabled and not all([ps3_endpoint, ps3_region, ps3_bucket, ps3_access_key, ps3_secret_key, ps3_cdn_endpoint]):
                request.session.flash(("All bucket fields are required when enabling BYOB.", "error"))
            elif ps3_endpoint and not ps3_endpoint.startswith("https://"):
                request.session.flash(("Bucket endpoint must start with https://", "error"))
            elif ps3_cdn_endpoint and not ps3_cdn_endpoint.startswith("https://"):
                request.session.flash(("CDN endpoint must start with https://", "error"))
            else:
                changed = False
                for attr, val in [
                    ("primary_s3_endpoint", ps3_endpoint),
                    ("primary_s3_region", ps3_region),
                    ("primary_s3_bucket", ps3_bucket),
                    ("primary_s3_access_key", ps3_access_key),
                    ("primary_s3_secret_key", ps3_secret_key),
                    ("primary_s3_cdn_endpoint", ps3_cdn_endpoint),
                ]:
                    if getattr(shop, attr) != val:
                        setattr(shop, attr, val)
                        changed = True
                if shop.primary_s3_enabled != ps3_enabled:
                    shop.primary_s3_enabled = ps3_enabled
                    changed = True
                if changed:
                    if ps3_enabled:
                        # Test connection when enabling
                        try:
                            import boto3
                            test_client = boto3.session.Session().client(
                                "s3",
                                region_name=ps3_region,
                                endpoint_url=ps3_endpoint,
                                aws_access_key_id=ps3_access_key,
                                aws_secret_access_key=ps3_secret_key,
                            )
                            test_client.list_objects_v2(Bucket=ps3_bucket, MaxKeys=0)
                            request.session.flash(("Storage bucket settings saved and connection verified.", "success"))
                        except Exception as e:
                            request.session.flash((f"Bucket settings saved but connection test failed: {e}", "error"))
                    else:
                        request.session.flash(("Storage bucket settings updated.", "success"))

        if form_section == "torrent-settings":
            # MPS-22: gated by torrent kill switch
            if not request.torrent_enabled:
                raise HTTPNotFound()
            torrent_enabled = checkbox_to_bool(
                request.params.get("torrent_enabled_checkbox", "off")
            )
            changed = False
            if shop.torrent_enabled != torrent_enabled:
                shop.torrent_enabled = torrent_enabled
                changed = True
            if changed:
                status = "enabled" if torrent_enabled else "disabled"
                request.session.flash((f"Torrent distribution {status}.", "success"))
                if torrent_enabled:
                    # Backfill: generate torrents for all existing products that
                    # have a product file but no magnet link yet.
                    _torrent_backfill_async(request, shop)

        # MPS-21: make-an-offer settings
        if form_section == "offer-settings":
            offer_enabled = checkbox_to_bool(
                request.params.get("offer_enabled_checkbox", "off")
            )
            if shop.offer_enabled != offer_enabled:
                shop.offer_enabled = offer_enabled
                status = "enabled" if offer_enabled else "disabled"
                request.session.flash((f"Make-an-offer {status}.", "success"))

            # Numeric thresholds — silently clamp invalid input rather than
            # rejecting the whole form.
            def _int_param(name, default, lo=0, hi=10**9):
                raw = (request.params.get(name) or "").strip()
                if not raw:
                    return default
                try:
                    val = int(float(raw))
                except (TypeError, ValueError):
                    return default
                return max(lo, min(hi, val))

            new_accept = _int_param(
                "offer_auto_accept_threshold_pct",
                shop.offer_auto_accept_threshold_pct, 0, 100,
            )
            new_decline = _int_param(
                "offer_auto_decline_threshold_pct",
                shop.offer_auto_decline_threshold_pct, 0, 100,
            )
            # Auto-decline must be strictly below auto-accept, else thresholds
            # collide and the queue range disappears. Clamp.
            if new_decline >= new_accept:
                new_decline = max(0, new_accept - 1)

            shop.offer_auto_accept_threshold_pct = new_accept
            shop.offer_auto_decline_threshold_pct = new_decline
            shop.offer_expiration_hours = _int_param(
                "offer_expiration_hours", shop.offer_expiration_hours,
                1, 24 * 365,
            )
            shop.offer_acceptance_payment_hours = _int_param(
                "offer_acceptance_payment_hours",
                shop.offer_acceptance_payment_hours,
                1, 24 * 30,
            )
            shop.offer_max_rounds = _int_param(
                "offer_max_rounds", shop.offer_max_rounds, 1, 100,
            )
            shop.offer_min_buyer_account_age_hours = _int_param(
                "offer_min_buyer_account_age_hours",
                shop.offer_min_buyer_account_age_hours, 0, 24 * 365,
            )

            # offer_min_in_cents — dollar input, optional.
            offer_min_dollars = (request.params.get("offer_min") or "").strip()
            if offer_min_dollars:
                try:
                    shop.offer_min_in_cents = int(round(float(offer_min_dollars) * 100))
                except (TypeError, ValueError):
                    request.session.flash(("Invalid offer minimum.", "error"))
            else:
                shop.offer_min_in_cents = None

    # If we processed any form submission, respond accordingly
    if form_section:
        if request.headers.get("X-Requested-With") == "XMLHttpRequest":
            messages = list(request.session.pop_flash())
            return Response(
                json={"status": "ok", "messages": messages},
                content_type="application/json",
            )
        return HTTPFound(f"/s/{shop.id}/settings")

    # TODO: Dry out this block, it's a copy pasta from views/product.py
    if s3_webhook_key and s3_webhook_bucket and s3_webhook_etag:
        # Check if the file exists and has a non-zero size
        try:
            response = request.shop_uploads_client.head_object(
                Bucket=s3_webhook_bucket,
                Key=s3_webhook_key,
            )
            file_size = response.get("ContentLength", 0)
            if file_size == 0:
                raise ValueError("File size is zero")
        except Exception:
            request.session.flash(
                ("File upload failed. Pick a file and try again.", "error")
            )
            return HTTPFound(request.route_url("shop_settings", shop_id=shop.id))

        # get file_from S3 service posted params.
        file_key = s3_webhook_key.split("/")[-1].split(".")[0]

        acl = "public-read"

        # don't allow proxies to cache and set the max age to 2 days.
        cache_control = "private, max-age=172800"

        # determine content type from extension.
        file_ext = s3_webhook_key.split(".")[-1]
        # print(file_ext)
        content_type = mimetypes.types_map.get("." + file_ext)
        # print(content_type)

        # copy upload to our system defined s3 location.
        # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.copy_object
        request.shop_uploads_client.copy_object(
            ACL=acl,
            Bucket=request.shop_bucket_name,
            CopySource={
                "Bucket": s3_webhook_bucket,
                "Key": s3_webhook_key,
            },
            ContentDisposition=f"inline; filename={file_key}",
            ContentType=content_type,
            CacheControl=cache_control,
            Key=f"{shop.id}/meta/{file_key}",
            MetadataDirective="REPLACE",
        )

        # Mirror shop asset to custom S3 bucket if configured
        from ..lib.s3_mirror import mirror_key_async
        mirror_key_async(
            request.shop_uploads_client,
            request.shop_bucket_name,
            f"{shop.id}/meta/{file_key}",
            shop,
            content_type=content_type,
            cache_control=cache_control,
        )

        # delete original upload key.
        request.shop_uploads_client.delete_object(
            Bucket=s3_webhook_bucket,
            Key=s3_webhook_key,
        )

        if file_key == "shop-favicon":
            shop.favicon = True

        if file_key == "shop-logo-banner":
            shop.logo_banner = True

        shop.stamp_updated_timestamp()

        # redirect back to this page to clear
        # the params posted by the s3 webhooks.
        return HTTPFound(f"/s/{shop.id}/settings?uploaded={file_key}")

    signed_posts = {}
    get_endpoints = {}

    for file_key in ["shop-logo-banner", "shop-logo-avatar", "shop-favicon"]:
        # conditionally sign the ability to POST to the object path when the
        # object path start's with the shop's s3 namespace (uuid) and only for
        # particular file keys. This prevents a user from writing files into
        # a shops namespace which is owned by a different user.
        key_starts_with = f"{shop.id}/meta/{file_key}.$filename"
        conditions = [
            {"success_action_redirect": shop.absolute_settings_url(request)},
            ["starts-with", "$key", key_starts_with],
        ]

        signed_posts[file_key] = request.shop_uploads_client.generate_presigned_post(
            Bucket=request.shop_bucket_name,
            # uploads to /<shop-uuid>/meta/shop-logo-banner.the-users-file.png
            Key=key_starts_with + "${filename}",
            ExpiresIn=900,
            Conditions=conditions,
        )
        get_endpoints[file_key] = "{}/{}/meta/{}".format(
            request.shop_cdn_endpoint,
            shop.id,
            file_key,
        )

        # We must create a field key/value for each extra condition.
        # unfortunately `conditions` is a list of single key/value dicts
        # and/or 2/3 item lists so we flatten them into a single fields dict.
        # [{1:2}, [3,4]] -> {1:2, 3:4}
        for condition in conditions:
            if isinstance(condition, list):
                if len(condition) == 2:
                    # convert list into dict.
                    condition = {condition[0]: condition[1]}
                else:
                    continue
            signed_posts[file_key]["fields"].update(condition)

    # Get crypto processor for Monero if enabled
    xmr_processor = None
    if request.monero_enabled:
        from ..models.crypto_processor import CryptoProcessor

        xmr_processor = (
            request.dbsession.query(CryptoProcessor)
            .filter(
                CryptoProcessor.shop_id == shop.id, CryptoProcessor.coin_type == "XMR"
            )
            .first()
        )

    # Get crypto processor for Dogecoin if enabled
    doge_processor = None
    if request.dogecoin_enabled:
        from ..models.crypto_processor import CryptoProcessor

        doge_processor = (
            request.dbsession.query(CryptoProcessor)
            .filter(
                CryptoProcessor.shop_id == shop.id, CryptoProcessor.coin_type == "DOGE"
            )
            .first()
        )

    return {
        "name": shop.name,
        "description": shop.description,
        "billing_address": shop.billing_address,
        "phone_number": shop.phone_number,
        "domain_name": shop.domain_name or "",
        "ribbon_text": shop.ribbon_text or "",
        "ribbon_text_color": shop.ribbon_text_color or "",
        "ribbon_color_1": shop.ribbon_color_1 or "",
        "ribbon_color_2": shop.ribbon_color_2 or "",
        "default_theme": shop.default_theme,
        "google_analytics_id": shop.google_analytics_id or "",
        "plausible_domain_name": shop.plausible_domain_name or "",
        "google_site_verification": shop.google_site_verification or "",
        "stripe_public_api_key": shop.stripe_public_api_key or "",
        "stripe_secret_api_key": shop.stripe_secret_api_key or "",
        "paypal_client_id": shop.paypal_client_id or "",
        "paypal_secret": shop.paypal_secret or "",
        "adyen_api_key": shop.adyen_api_key or "",
        "adyen_merchant_account": shop.adyen_merchant_account or "",
        "adyen_client_key": shop.adyen_client_key or "",
        "adyen_hmac_key": shop.adyen_hmac_key or "",
        "adyen_enabled": shop.adyen_enabled,
        "crypto_quote_expiry_seconds": shop.crypto_quote_expiry_seconds,
        "payment_risk_threshold_mid_dollars": cents_to_dollars(
            shop.payment_risk_threshold_mid_cents
        ),
        "payment_risk_threshold_high_dollars": cents_to_dollars(
            shop.payment_risk_threshold_high_cents
        ),
        "xmr_processor": xmr_processor,
        "doge_processor": doge_processor,
        "unsandbox_public_key": shop.unsandbox_public_key or "",
        "unsandbox_secret_key": shop.unsandbox_secret_key or "",
        "mirror_s3_endpoint": shop.mirror_s3_endpoint or "",
        "mirror_s3_region": shop.mirror_s3_region or "",
        "mirror_s3_bucket": shop.mirror_s3_bucket or "",
        "mirror_s3_access_key": shop.mirror_s3_access_key or "",
        "mirror_s3_secret_key": shop.mirror_s3_secret_key or "",
        "mirror_s3_enabled": shop.mirror_s3_enabled,
        "gift_card_enabled": shop.gift_card_enabled,
        "gift_card_min_dollars": cents_to_dollars(shop.gift_card_min_in_cents),
        "gift_card_max_dollars": cents_to_dollars(shop.gift_card_max_in_cents),
        "environment": shop.environment,
        "environment_label": shop.environment_label,
        "primary_s3_endpoint": shop.primary_s3_endpoint or "",
        "primary_s3_region": shop.primary_s3_region or "",
        "primary_s3_bucket": shop.primary_s3_bucket or "",
        "primary_s3_access_key": shop.primary_s3_access_key or "",
        "primary_s3_secret_key": shop.primary_s3_secret_key or "",
        "primary_s3_cdn_endpoint": shop.primary_s3_cdn_endpoint or "",
        "primary_s3_enabled": shop.primary_s3_enabled,
        # MPS-22: torrent kill switch hides shop's per-shop opt-in from settings UI
        "torrent_enabled": shop.torrent_enabled if request.torrent_enabled else False,
        "signed_posts": signed_posts,
        "get_endpoints": get_endpoints,
        "api_keys": shop.api_keys.filter_by(is_active=True).order_by("created_timestamp").all(),
    }


# ── API key management ────────────────────────────────────────────────────────

@view_config(route_name="shop_api_keys_generate", renderer="json", request_method="POST")
@user_required()
@shop_owner_required()
def api_keys_generate(request):
    """Generate a new HMAC key pair for the shop. Returns secret once."""
    from ..models.api_key import MpsApiKey

    label = request.params.get("label", "").strip()[:128]
    shop = request.shop

    api_key, secret_plaintext = MpsApiKey.generate(shop, label=label)
    request.dbsession.add(api_key)
    request.dbsession.flush()

    # Flash the secret — shown once, never retrievable again
    request.session.flash((
        f"API key generated. Public key: {api_key.public_key}  "
        f"Secret (copy now — shown once): {secret_plaintext}",
        "api-secret",
    ))
    return HTTPFound(request.route_url("shop_settings", shop_id=shop.id))


@view_config(route_name="shop_api_key_revoke", renderer="json", request_method="POST")
@user_required()
@shop_owner_required()
def api_key_revoke(request):
    """Revoke (deactivate) an API key."""
    from ..models.api_key import MpsApiKey

    key_id = request.matchdict["key_id"]
    shop = request.shop

    api_key = (
        request.dbsession.query(MpsApiKey)
        .filter_by(id=key_id, shop_id=shop.id, is_active=True)
        .first()
    )
    if api_key is None:
        request.session.flash(("API key not found.", "error"))
        return HTTPFound(request.route_url("shop_settings", shop_id=shop.id))

    api_key.is_active = False
    request.dbsession.add(api_key)
    request.session.flash(("API key revoked.", "success"))
    return HTTPFound(request.route_url("shop_settings", shop_id=shop.id))


# ── Torrent backfill progress ─────────────────────────────────────────────────

@view_config(route_name="shop_torrent_backfill_status", renderer="json", request_method="GET")
@user_required()
@shop_owner_required()
def torrent_backfill_status(request):
    """Return torrent backfill progress for the shop.

    JSON: { total, done, remaining }
    - total: products that have a product file uploaded
    - done: of those, how many already have a magnet link
    - remaining: total - done
    """
    if not request.torrent_enabled:
        raise HTTPNotFound()

    from ..models.product import Product

    products = (
        request.dbsession.query(Product)
        .filter(Product.shop_id == request.shop.id)
        .all()
    )

    def _eligible(p):
        if not p.torrent_opt_in or p.visibility != 1:
            return False
        # Paid products need a preview file; free content needs the product file.
        if p.is_sellable:
            return "preview" in p.originals
        return "product" in p.originals

    eligible = [p for p in products if _eligible(p)]
    done = [p for p in eligible if p.torrent_magnet_link]

    total = len(eligible)
    done_count = len(done)
    return {
        "total": total,
        "done": done_count,
        "remaining": total - done_count,
    }


# ── Checksum backfill ─────────────────────────────────────────────────────────

_CHECKSUM_FILE_KEYS = ["product", "preview", "thumbnail1", "thumbnail2", "thumbnail3", "thumbnail4"]


@view_config(route_name="shop_checksum_backfill", renderer="json", request_method="POST")
@user_required()
@shop_owner_required()
def checksum_backfill(request):
    """Trigger background checksum computation for all files that don't have one yet."""
    _checksum_backfill_async(request, request.shop)
    request.session.flash(("Checksum backfill started.", "success"))
    return HTTPFound(request.route_url("shop_settings", shop_id=request.shop.id))


@view_config(route_name="shop_checksum_backfill_status", renderer="json", request_method="GET")
@user_required()
@shop_owner_required()
def checksum_backfill_status(request):
    """Return checksum backfill progress.

    JSON: { total, done, remaining }
    - total: count of (product, file_key) pairs that have a file
    - done:  of those, how many have both md5 + sha256
    """
    from ..models.product import Product

    products = (
        request.dbsession.query(Product)
        .filter(Product.shop_id == request.shop.id)
        .all()
    )

    total = 0
    done_count = 0
    for p in products:
        for file_key in _CHECKSUM_FILE_KEYS:
            if file_key not in p.originals:
                continue
            total += 1
            cs = p.checksums.get(file_key, {})
            if cs.get("md5") and cs.get("sha256"):
                done_count += 1

    return {
        "total": total,
        "done": done_count,
        "remaining": total - done_count,
    }
