import logging

from pyramid.httpexceptions import HTTPNotFound
from pyramid.view import view_config

from ..models.product import get_media_type, get_related_products, get_ring_related_products
from ..models.shop import get_shop_by_id, validate_discovery_ring
from ..lib.cache_version import compute_cache_version
from ..lib.currency import cents_to_dollars
from ..lib.time_funcs import timestamp_to_ago_string, timestamp_to_datetime

log = logging.getLogger(__name__)


@view_config(route_name="watch_json", renderer="json")
def watch_json(request):
    """JSON endpoint for watch mode SPA navigation."""

    product = request.product

    # Compute cache version up front so every response (including errors)
    # carries it — lets the client flush stale localStorage ring state
    # even when the current product can't be played.
    shop_for_cache = request.shop if request.shop else (product.shop if product else None)
    cache_version = compute_cache_version(shop_for_cache)

    if not product:
        request.response.status_int = 404
        return {"error": "Product not found", "cache_version": cache_version}

    if not request.shop or not request.shop.watch_mode_enabled:
        request.response.status_int = 404
        return {"error": "Watch mode not enabled", "cache_version": cache_version}

    # Only allow public products (visibility == 1)
    if product.visibility != 1:
        request.response.status_int = 403
        return {"error": "Not publicly available", "cache_version": cache_version}

    # Determine which file to use
    if product.is_sellable:
        file_key = "preview"
        if file_key not in product.extensions:
            request.response.status_int = 403
            return {"error": "No preview available", "cache_version": cache_version}
    else:
        file_key = "product"
        if file_key not in product.extensions:
            request.response.status_int = 404
            return {"error": "No media file available", "cache_version": cache_version}

    # Get extension and detect media type
    extension = product.extensions.get(file_key)
    media_type = get_media_type(extension) or "other"

    # Generate presigned URL for media
    bucket_name = request.shop_bucket_name
    s3_key = f"{product.s3_path}/{file_key}"

    params = {
        "Bucket": bucket_name,
        "Key": s3_key,
    }

    content_disposition = f'inline; filename="{product.title}.{extension}"'
    content_type = product.get_content_type(file_key)

    if content_disposition:
        params["ResponseContentDisposition"] = content_disposition

    if content_type:
        params["ResponseContentType"] = content_type

    media_url = request.shop_uploads_client.generate_presigned_url(
        ClientMethod="get_object",
        Params=params,
        ExpiresIn=900,
    )

    # Generate presigned URLs for karaoke tracks if available.
    # Global karaoke kill switch (MPS-22) gates all karaoke surfaces.
    instrumentals_url = None
    vocals_url = None
    karaoke_eligible = False
    if request.karaoke_enabled and media_type in ("video", "audio"):
        shop = request.shop
        karaoke_eligible = bool(shop.unsandbox_public_key and shop.unsandbox_secret_key)
        for track_name in ("instrumentals", "vocals"):
            if track_name in product.extensions:
                track_ext = product.extensions.get(track_name)
                track_ct = product.get_content_type(track_name) or (
                    "video/mp4" if media_type == "video" else "audio/wav"
                )
                url = request.shop_uploads_client.generate_presigned_url(
                    ClientMethod="get_object",
                    Params={
                        "Bucket": bucket_name,
                        "Key": f"{product.s3_path}/{track_name}",
                        "ResponseContentDisposition": f'inline; filename="{product.title} ({track_name.title()}).{track_ext}"',
                        "ResponseContentType": track_ct,
                    },
                    ExpiresIn=900,
                )
                if track_name == "instrumentals":
                    instrumentals_url = url
                else:
                    vocals_url = url

    # Thumbnail URL
    thumbnail_url = None
    if "thumbnail1" in product.extensions:
        thumbnail_url = (
            f"{request.shop_cdn_endpoint}"
            f"/{product.s3_path}/thumbnail1"
            f"?ts={product.updated_timestamp}"
        )

    # Canonical URL (relative path)
    p_or_c = "p" if product.is_sellable else "c"
    canonical_url = f"/{p_or_c}/{product.id}/{product.slug}"

    # Use existing discovery ring — never recompute during a request
    ring = request.shop.discovery_ring
    if ring:
        # Client sends dir=-1 when traveling backward through the ring.
        # Flip forward/backward so the sidebar shows items in the travel direction.
        try:
            direction = int(request.params.get("dir", "1"))
        except (ValueError, TypeError):
            direction = 1
        if direction == -1:
            related = get_ring_related_products(product, ring, forward=3, backward=42)
        else:
            related = get_ring_related_products(product, ring, forward=42, backward=3)
    else:
        related = get_related_products(product)

    related_data = []
    for item in related:
        if isinstance(item, dict):
            r = item["product"]
            offset = item["offset"]
        else:
            r = item
            offset = 1
        r_ext = r.extensions.get("product", "")
        r_media = get_media_type(r_ext)
        r_thumb = None
        if "thumbnail1" in r.extensions:
            r_thumb = (
                f"{request.shop_cdn_endpoint}"
                f"/{r.s3_path}/thumbnail1"
                f"?ts={r.updated_timestamp}"
            )
        r_p_or_c = "p" if r.is_sellable else "c"
        related_data.append({
            "id": str(r.id),
            "title": r.title,
            "thumbnail_url": r_thumb,
            "url": f"/{r_p_or_c}/{r.id}/{r.slug}",
            "media_type": r_media,
            "offset": offset,
        })

    # Generate download URL (attachment disposition) for the product file
    download_url = None
    has_product_file = product.has_product_file
    file_type_str = ""
    file_size_str = ""
    if has_product_file:
        dl_content_disposition = product.get_content_disposition(file_key)
        dl_content_type = product.get_content_type(file_key)
        dl_params = {"Bucket": bucket_name, "Key": s3_key}
        if dl_content_disposition:
            # Force attachment for download
            dl_params["ResponseContentDisposition"] = dl_content_disposition.replace(
                "inline;", "attachment;"
            )
        if dl_content_type:
            dl_params["ResponseContentType"] = dl_content_type
            file_type_str = dl_content_type
        file_size_str = product.human_file_bytes(file_key)
        download_url = request.shop_uploads_client.generate_presigned_url(
            ClientMethod="get_object",
            Params=dl_params,
            ExpiresIn=900,
        )

    # Public CDN URL for the product file (thumbnail-to-file links)
    file_url = None
    if has_product_file:
        file_url = (
            f"{request.shop_cdn_endpoint}"
            f"/{product.s3_path}/{file_key}"
            f"?ts={product.updated_timestamp}"
        )

    # Check if current user is a shop editor (mod)
    is_mod = False
    if request.user and request.user.authenticated:
        is_mod = request.user.can_edit_shop(request.shop)

    # Build price history for mod users
    price_history = []
    if is_mod and product.is_sellable:
        history_rows = product.price_history.limit(20).all()
        for i, ph in enumerate(history_rows):
            price_history.append({
                "price_formatted": f"${cents_to_dollars(ph.price_in_cents):.2f}",
                "ago": timestamp_to_ago_string(ph.created_timestamp),
                "datetime": str(timestamp_to_datetime(ph.created_timestamp)),
                "is_current": i == 0,
            })

    result = {
        "cache_version": compute_cache_version(request.shop),
        "product_id": str(product.id),
        "shop_id": str(product.shop_id),
        "title": product.title,
        "media_url": media_url,
        "media_type": media_type,
        "thumbnail_url": thumbnail_url,
        "description_html": product.description_html,
        "canonical_url": canonical_url,
        "is_sellable": product.is_sellable,
        "has_product_file": has_product_file,
        "download_url": download_url,
        "file_url": file_url,
        "file_type": file_type_str,
        "file_size": file_size_str,
        "view_count": product.view_count or 0,
        "human_view_count": product.human_view_count,
        "human_created_timestamp": product.human_created_timestamp,
        "human_updated_timestamp": product.human_updated_timestamp,
        "show_dates": request.shop.show_dates,
        "related": related_data,
        "ring": ring,
        "is_mod": is_mod,
        "comment_count": product.public_comment_count,
        "comments_enabled": request.shop.comments_enabled,
        "instrumentals_url": instrumentals_url,
        "vocals_url": vocals_url,
        "karaoke_eligible": karaoke_eligible,
    }

    if price_history:
        result["price_history"] = price_history

    return result


@view_config(route_name="karaoke_process", renderer="json", request_method="POST")
def karaoke_process(request):
    """On-demand karaoke processing for a single product.

    Validates the request, then hands off to process_karaoke_detached
    which double-forks and runs karaoke in a detached child. The
    watch_json 10s refresh loop picks up the new URLs when done.
    """
    if not request.karaoke_enabled:
        raise HTTPNotFound()

    product_id = request.matchdict.get("product_id")
    from ..models.product import Product
    try:
        product = request.dbsession.query(Product).get(product_id)
    except Exception:
        product = None

    if not product:
        request.response.status_int = 404
        return {"error": "Product not found"}

    shop = product.shop

    if not shop.unsandbox_public_key or not shop.unsandbox_secret_key:
        request.response.status_int = 400
        return {"error": "Karaoke not configured for this shop"}

    file_key = "preview" if product.is_sellable else "product"

    extension = product.extensions.get(file_key)
    if not extension:
        request.response.status_int = 400
        return {"error": "No media file"}

    media_type = get_media_type(extension)
    if media_type not in ("video", "audio"):
        request.response.status_int = 400
        return {"error": "Not audio or video"}

    # Already has tracks?
    if "instrumentals" in product.extensions and "vocals" in product.extensions:
        return {"status": "ready"}

    from ..lib.karaoke import capture_karaoke_config, process_karaoke_detached
    process_karaoke_detached(
        product_id=product_id,
        file_key=file_key,
        extension=extension,
        s3_path=product.s3_path,
        karaoke_config=capture_karaoke_config(shop, request.app),
        db_url=str(request.dbsession.get_bind().url),
    )
    return {"status": "processing"}


@view_config(route_name="discovery_ring_json", renderer="json")
def discovery_ring_json(request):
    """Public JSON endpoint to inspect the discovery ring for a shop."""
    shop_id = request.matchdict.get("shop_id")
    shop = get_shop_by_id(request.dbsession, shop_id)
    if not shop:
        request.response.status_int = 404
        return {"error": "Shop not found"}

    ring = shop.discovery_ring
    return {
        "shop_id": str(shop.id),
        "shop_name": shop.name,
        "watch_mode_enabled": shop.watch_mode_enabled,
        "ring_length": len(ring),
        "ring": ring,
    }


@view_config(route_name="discovery_ring_health", renderer="json")
def discovery_ring_health(request):
    """Mod-only JSON diagnostic for discovery ring topology.

    Returns duplicates, orphans (public products missing from ring),
    stale (ring IDs no longer public), and length mismatch flags.
    """
    shop_id = request.matchdict.get("shop_id")
    shop = get_shop_by_id(request.dbsession, shop_id)
    if not shop:
        request.response.status_int = 404
        return {"error": "Shop not found"}

    if not (request.user and request.user.authenticated
            and request.user.can_edit_shop(shop)):
        request.response.status_int = 403
        return {"error": "Shop editor access required"}

    health = validate_discovery_ring(shop)
    health["shop_id"] = str(shop.id)
    health["shop_name"] = shop.name
    return health
