from werkzeug.exceptions import HTTPException
from shared.utils import hx_fragment_request

from quart import (
    request,
    render_template,
    make_response,
    current_app
)

from markupsafe import escape
from shared.infrastructure.fragments import FragmentError as _FragmentError

class AppError(ValueError):
    """
    Base class for app-level, client-safe errors.
    Behaves like ValueError so existing except ValueError: still works.
    """
    status_code: int = 400

    def __init__(self, message, *, status_code: int | None = None):
        # Support a single message or a list/tuple of messages
        if isinstance(message, (list, tuple, set)):
            self.messages = [str(m) for m in message]
            msg = self.messages[0] if self.messages else ""
        else:
            self.messages = [str(message)]
            msg = str(message)

        super().__init__(msg)

        if status_code is not None:
            self.status_code = status_code


def _error_page(message: str) -> str:
    """Self-contained error page HTML. Bypasses Jinja/context processors."""
    return (
        "<!DOCTYPE html><html><head><meta charset='utf-8'>"
        "<meta name='viewport' content='width=device-width,initial-scale=1'>"
        "<title>Error</title>"
        "<style>"
        "body{margin:0;min-height:100vh;display:flex;align-items:center;"
        "justify-content:center;font-family:system-ui,sans-serif;"
        "background:#fafafa;color:#333}"
        ".box{text-align:center;padding:2rem;max-width:480px}"
        ".box h1{font-size:1.5rem;color:#ef4444;margin:0 0 1rem}"
        ".box p{margin:0 0 1.5rem;line-height:1.6}"
        ".box a{color:#3b82f6;text-decoration:none}"
        ".box img{max-width:300px;margin:1rem auto}"
        "</style></head><body>"
        "<div class='box'>"
        f"<h1>{message}</h1>"
        "<img src='/static/errors/error.gif' width='300' height='300'>"
        "<p><a href='javascript:location.reload()'>Reload</a></p>"
        "</div></body></html>"
    )


def _sx_error_page(errnum: str, message: str, image: str | None = None) -> str:
    """Render an error page via s-expressions. Bypasses Jinja entirely."""
    from shared.sx.page import render_page

    return render_page(
        '(~shared:pages/error-page :title title :message message :image image :asset-url "/static")',
        title=f"{errnum} Error",
        message=message,
        image=image,
    )


async def _rich_error_page(errnum: str, message: str, image: str | None = None) -> str | None:
    """Try to render an error page with site headers and post breadcrumb.

    Returns HTML string on success, None if we should fall back to the
    minimal error page.  All failures are swallowed — this must never
    make a bad situation worse.
    """
    from quart import g

    # Skip for internal/static requests
    if request.path.startswith(("/internal/", "/static/", "/auth/")):
        return None

    try:
        # Build a minimal context — avoid get_template_context() which
        # calls cross-service fragment fetches that may fail.
        from shared.infrastructure.context import base_context
        try:
            ctx = await base_context()
        except Exception:
            ctx = {"base_title": "Rose Ash", "asset_url": "/static"}

        # Inject Jinja globals (blog_url, cart_url, etc.) — these are
        # needed by call_url() for cross-subdomain links.
        for key, val in current_app.jinja_env.globals.items():
            if key not in ctx:
                ctx[key] = val

        # Try to fetch fragments, but don't fail if they're unreachable
        try:
            from shared.infrastructure.fragments import fetch_fragments
            from shared.infrastructure.cart_identity import current_cart_identity
            user = getattr(g, "user", None)
            ident = current_cart_identity()
            cart_params = {}
            if ident["user_id"] is not None:
                cart_params["user_id"] = ident["user_id"]
            if ident["session_id"] is not None:
                cart_params["session_id"] = ident["session_id"]
            cart_mini, auth_menu, nav_tree = await fetch_fragments([
                ("cart", "cart-mini", cart_params or None),
                ("account", "auth-menu", {"email": user.email} if user else None),
                ("blog", "nav-tree", {"app_name": current_app.name, "path": request.path}),
            ])
            ctx["cart_mini"] = cart_mini
            ctx["auth_menu"] = auth_menu
            ctx["nav_tree"] = nav_tree
        except Exception:
            ctx.setdefault("cart_mini", "")
            ctx.setdefault("auth_menu", "")
            ctx.setdefault("nav_tree", "")

        # Try to hydrate post data from slug if not already available
        segments = [s for s in request.path.strip("/").split("/") if s]
        slug = segments[0] if segments else None
        post_data = getattr(g, "post_data", None)

        if slug and not post_data:
            try:
                from shared.infrastructure.data_client import fetch_data
                raw = await fetch_data(
                    "blog", "post-by-slug",
                    params={"slug": slug},
                    required=False,
                )
                if raw:
                    post_data = {
                        "post": {
                            "id": raw["id"],
                            "title": raw["title"],
                            "slug": raw["slug"],
                            "feature_image": raw.get("feature_image"),
                        },
                    }
            except Exception:
                pass

        # Root header (site nav bar)
        from shared.sx.helpers import (
            root_header_sx, post_header_sx,
            header_child_sx, full_page_sx, sx_call,
        )
        hdr = await root_header_sx(ctx)

        # Post breadcrumb if we resolved a post
        post = (post_data or {}).get("post") or ctx.get("post") or {}
        if post.get("slug"):
            ctx["post"] = post
            post_row = await post_header_sx(ctx)
            if post_row:
                hdr = "(<> " + hdr + " " + await header_child_sx(post_row) + ")"

        # Error content
        error_content = sx_call("error-content", errnum=errnum, message=message, image=image)

        return await full_page_sx(ctx, header_rows=hdr, content=error_content)
    except Exception:
        current_app.logger.debug("Rich error page failed, falling back", exc_info=True)
        return None


def errors(app):
    def _info(e):
        return {
            "exception": e,
            "method": request.method,
            "url": str(request.url),
            "base_url": str(request.base_url),
            "root_path": request.root_path,
            "path": request.path,
            "full_path": request.full_path,
            "endpoint": request.endpoint,
            "url_rule": str(request.url_rule) if request.url_rule else None,
            "headers": {k: v for k, v in request.headers.items()
                        if k.lower().startswith("x-forwarded") or k in ("Host",)},
        }

    @app.errorhandler(404)
    async def not_found(e):
        current_app.logger.warning("404 %s", _info(e))
        if hx_fragment_request():
            html = await render_template(
                "_types/root/exceptions/hx/_.html",
                errnum='404'
            )
        else:
            # Try a rich error page with site headers + post breadcrumb
            html = await _rich_error_page(
                "404", "NOT FOUND",
                image="/static/errors/404.gif",
            )
            if html is None:
                try:
                    html = _sx_error_page(
                        "404", "NOT FOUND",
                        image="/static/errors/404.gif",
                    )
                except Exception:
                    html = await render_template(
                        "_types/root/exceptions/_.html",
                        errnum='404',
                    )

        return await make_response(html, 404)

    @app.errorhandler(403)
    async def not_allowed(e):
        current_app.logger.warning("403 %s", _info(e))
        if hx_fragment_request():
            html = await render_template(
                "_types/root/exceptions/hx/_.html",
                errnum='403'
            )
        else:
            html = await _rich_error_page(
                "403", "FORBIDDEN",
                image="/static/errors/403.gif",
            )
            if html is None:
                try:
                    html = _sx_error_page(
                        "403", "FORBIDDEN",
                        image="/static/errors/403.gif",
                    )
                except Exception:
                    html = await render_template(
                        "_types/root/exceptions/_.html",
                        errnum='403',
                    )

        return await make_response(html, 403)

    @app.errorhandler(AppError)
    async def app_error(e: AppError):
        # App-level, client-safe errors
        current_app.logger.info("AppError %s", _info(e))
        status = getattr(e, "status_code", 400)
        messages = getattr(e, "messages", [str(e)])

        if request.headers.get("SX-Request") == "true" or request.headers.get("HX-Request") == "true":
            from shared.sx.jinja_bridge import render as render_comp
            items = "".join(
                render_comp("error-list-item", message=str(escape(m)))
                for m in messages if m
            )
            html = render_comp("error-list", items_html=items)
            return await make_response(html, status)

        # Non-HTMX: show a nicer page with error messages
        html = await render_template(
            "_types/root/exceptions/app_error.html",
            messages=messages,
        )
        return await make_response(html, status)

    @app.errorhandler(_FragmentError)
    async def fragment_error(e):
        current_app.logger.error("FragmentError %s", _info(e))
        msg = str(e)
        # Extract service name from "Fragment account/auth-menu failed: ..."
        service = msg.split("/")[0].replace("Fragment ", "") if "/" in msg else "unknown"
        if request.headers.get("SX-Request") == "true" or request.headers.get("HX-Request") == "true":
            from shared.sx.jinja_bridge import render as render_comp
            return await make_response(
                render_comp("fragment-error", service=str(escape(service))),
                503,
            )
        # Raw HTML — cannot use render_template here because the context
        # processor would try to fetch fragments again → infinite loop.
        return await make_response(_error_page(
            f"The <b>{escape(service)}</b> service is currently unavailable. It may be restarting."
        ), 503)

    @app.errorhandler(Exception)
    async def error(e):
        current_app.logger.exception("Exception %s", _info(e))

        status = 500
        if isinstance(e, HTTPException):
            status = e.code or 500

        if request.headers.get("SX-Request") == "true" or request.headers.get("HX-Request") == "true":
            return await make_response(
                "Something went wrong. Please try again.",
                status,
            )

        errnum = str(status)
        html = await _rich_error_page(
            errnum, "WELL THIS IS EMBARRASSING\u2026",
            image="/static/errors/error.gif",
        )
        if html is None:
            html = _error_page("WELL THIS IS EMBARRASSING&hellip;")
        return await make_response(html, status)
