"""External admin domain - HTTP routes.

Two route groups:
  /api/workspaces/{workspace_id}/projects/{project_id}/external-admin
      → dashboard-facing, requires authenticated P2HS user

  /v1/external-admin/...
      → plugin-facing, authenticated via per-installation bearer token
"""
from __future__ import annotations

from typing import Optional
from uuid import UUID

from fastapi import APIRouter, Depends, Header, Path, Query
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession

from src.config import get_settings as get_app_settings
from src.core.database import get_db
from src.core.redis import get_redis
from src.core.dependencies import require_active_billing_for_workspace_id
from src.core.edge_attestation import EDGE_SHOPIFY, EDGE_WP_GATEWAY
from src.core.edge_guard import require_edge
from src.core.exceptions import ForbiddenError
from src.domain.auth.models import User
from src.domain.auth.router import get_current_user
from src.domain.billing.access_policy import BillingAccessPolicy
from src.domain.billing.repository import SubscriptionRepository
from src.domain.external_admin.repository import (
    AssistantActionSessionRepository,
    AssistantDataConsentRepository,
    AssistantProjectSettingsRepository,
    ExternalAdminChatJobRepository,
    ExternalAdminChatMessageRepository,
    ExternalAdminChatSessionRepository,
    ExternalAdminInstallationRepository,
    ExternalCredentialRepository,
    SiteUnderstandingRunRepository,
    WpMutationActionRepository,
)
from src.domain.external_admin.schemas import (
    ActionSessionListResponse,
    ActionSessionReviewRequest,
    AssistantActionSessionResponse,
    AssistantDataConsentResponse,
    AssistantProjectSettingsResponse,
    AssistantProjectSettingsUpdate,
    CompanionInstallRequest,
    CompanionInstallResponse,
    ExternalAdminInstallationResponse,
    ExternalCredentialSummary,
    GrantConsentRequest,
    RotateCredentialRequest,
    RotateCredentialResponse,
    ShopifyApplyPollRequest,
    ShopifyApplyPollResponse,
    ShopifyApplyReportRequest,
    ShopifyApplyReportResponse,
    ShopifyChatJobPollRequest,
    ShopifyChatJobPollResponse,
    ShopifyChatMessageListRequest,
    ShopifyChatMessageListResponse,
    ShopifyChatMessageSendRequest,
    ShopifyChatMessageSendResponse,
    ShopifyChatSessionOpenRequest,
    ShopifyChatSessionOpenResponse,
    ShopifyConnectProjectRequest,
    ShopifyConnectProjectResponse,
    ShopifyHeartbeatRequest,
    ShopifyHeartbeatResponse,
    ShopifyOAuthCallbackRequest,
    ShopifyOAuthCallbackResponse,
    ShopifySiteUnderstandingStatusRequest,
    ShopifySiteUnderstandingStatusResponse,
    ShopifySiteUnderstandingSyncRequest,
    ShopifySiteUnderstandingSyncResponse,
    ShopifyWebhookEnvelope,
    WordPressConnectCodeRequest,
    WordPressConnectCodeResponse,
    WordPressConnectCompleteRequest,
    WordPressConnectCompleteResponse,
    WordPressEmbedExchangeRequest,
    WordPressEmbedExchangeResponse,
    WordPressConnectProjectRequest,
    WordPressConnectProjectResponse,
    WordPressConnectStartRequest,
    WordPressChatJobPollRequest,
    WordPressChatJobPollResponse,
    WordPressChatMessageListRequest,
    WordPressChatMessageListResponse,
    WordPressChatMessageSendRequest,
    WordPressChatMessageSendResponse,
    WordPressChatSessionOpenRequest,
    WordPressChatSessionOpenResponse,
    WordPressHeartbeatRequest,
    WordPressHeartbeatResponse,
    WordPressHostedUrlRequest,
    WordPressHostedUrlResponse,
    WordPressCompanionMutationPollRequest,
    WordPressCompanionMutationPollResponse,
    WordPressCompanionMutationReportRequest,
    WordPressCompanionMutationReportResponse,
    WordPressSiteUnderstandingStatusRequest,
    WordPressSiteUnderstandingStatusResponse,
    WordPressSiteUnderstandingSyncRequest,
    WordPressSiteUnderstandingSyncResponse,
)
from src.domain.external_admin.service import ExternalAdminService
from src.domain.external_admin.wordpress_connect import validate_wordpress_return_url
from src.domain.external_admin.wordpress_companion_package import (
    resolve_wordpress_companion_package,
)
from src.domain.projects.repository import ProjectRepository
from src.domain.projects.router import get_project_service
from src.domain.projects.service import ProjectService
from src.domain.workspaces.repository import WorkspaceRepository

# ── Dashboard-facing router ───────────────────────────────────────────────────

router = APIRouter(
    prefix="/api/workspaces/{workspace_id}/projects/{project_id}/external-admin",
    tags=["external-admin"],
)

# ── Plugin-facing router ──────────────────────────────────────────────────────

plugin_router = APIRouter(prefix="/v1/external-admin", tags=["external-admin-plugin"])

workspace_router = APIRouter(
    prefix="/api/workspaces/{workspace_id}/external-admin",
    tags=["external-admin"],
)


def _svc(db: AsyncSession = Depends(get_db)) -> ExternalAdminService:
    return ExternalAdminService(
        installation_repo=ExternalAdminInstallationRepository(db),
        credential_repo=ExternalCredentialRepository(db),
        settings_repo=AssistantProjectSettingsRepository(db),
        action_session_repo=AssistantActionSessionRepository(db),
        consent_repo=AssistantDataConsentRepository(db),
        project_repo=ProjectRepository(db),
        chat_session_repo=ExternalAdminChatSessionRepository(db),
        chat_message_repo=ExternalAdminChatMessageRepository(db),
        chat_job_repo=ExternalAdminChatJobRepository(db),
        site_understanding_run_repo=SiteUnderstandingRunRepository(db),
        mutation_action_repo=WpMutationActionRepository(db),
        billing_policy=BillingAccessPolicy(
            subscription_repo=SubscriptionRepository(db),
            workspace_repo=WorkspaceRepository(db),
        ),
    )


# ── Installation ──────────────────────────────────────────────────────────────

# Machine routes must arrive through a P2HS Edge Worker (contract 2A.2). Each Edge has its
# own key, so the WP Gateway cannot call Shopify routes and vice versa. In the default
# "report" rollout mode behaviour is unchanged (legacy static token); "enforce" is the target.
_require_wp_gateway = require_edge(EDGE_WP_GATEWAY, legacy_token=lambda s: s.wp_gateway_token)
_require_shopify_edge = require_edge(EDGE_SHOPIFY, legacy_token=lambda s: s.wp_gateway_token)
# Routes that had no static check before (connect start/complete): unchanged until "enforce".
_require_wp_edge_new = require_edge(EDGE_WP_GATEWAY, legacy_token=None)


@router.get("", response_model=ExternalAdminInstallationResponse)
async def get_installation(
    workspace_id: UUID = Depends(require_active_billing_for_workspace_id),
    project_id: UUID = Path(...),
    user: User = Depends(get_current_user),
    svc: ExternalAdminService = Depends(_svc),
):
    return await svc.get_installation(project_id, workspace_id)


# ── Settings ──────────────────────────────────────────────────────────────────

@router.get("/settings", response_model=AssistantProjectSettingsResponse)
async def get_settings(
    workspace_id: UUID = Depends(require_active_billing_for_workspace_id),
    project_id: UUID = Path(...),
    user: User = Depends(get_current_user),
    svc: ExternalAdminService = Depends(_svc),
):
    return await svc.get_settings(project_id, workspace_id)


@router.patch("/settings", response_model=AssistantProjectSettingsResponse)
async def update_settings(
    body: AssistantProjectSettingsUpdate,
    workspace_id: UUID = Depends(require_active_billing_for_workspace_id),
    project_id: UUID = Path(...),
    user: User = Depends(get_current_user),
    svc: ExternalAdminService = Depends(_svc),
):
    return await svc.update_settings(project_id, workspace_id, body)


# ── Credentials ───────────────────────────────────────────────────────────────

@router.get("/credentials", response_model=list[ExternalCredentialSummary])
async def list_credentials(
    workspace_id: UUID = Depends(require_active_billing_for_workspace_id),
    project_id: UUID = Path(...),
    user: User = Depends(get_current_user),
    svc: ExternalAdminService = Depends(_svc),
):
    return await svc.list_credentials(project_id, workspace_id)


@router.post("/credentials/rotate", response_model=RotateCredentialResponse)
async def rotate_credential(
    body: RotateCredentialRequest,
    workspace_id: UUID = Depends(require_active_billing_for_workspace_id),
    project_id: UUID = Path(...),
    user: User = Depends(get_current_user),
    svc: ExternalAdminService = Depends(_svc),
):
    cred = await svc.rotate_credential(project_id, workspace_id, body.credential_type, user.id)
    return RotateCredentialResponse(
        credential_id=cred.id,
        credential_type=cred.credential_type,
        version=cred.version,
        status=cred.status,
        created_at=cred.created_at,
    )


# ── Action sessions ────────────────────────────────────────────────────────────

@router.get("/action-sessions", response_model=ActionSessionListResponse)
async def list_action_sessions(
    workspace_id: UUID = Depends(require_active_billing_for_workspace_id),
    project_id: UUID = Path(...),
    user: User = Depends(get_current_user),
    status: Optional[str] = Query(default=None),
    limit: int = Query(default=50, ge=1, le=200),
    offset: int = Query(default=0, ge=0),
    svc: ExternalAdminService = Depends(_svc),
):
    items, total = await svc.list_action_sessions(project_id, workspace_id, status=status, limit=limit, offset=offset)
    return ActionSessionListResponse(items=items, total=total)


@router.get("/action-sessions/{session_id}", response_model=AssistantActionSessionResponse)
async def get_action_session(
    workspace_id: UUID = Depends(require_active_billing_for_workspace_id),
    project_id: UUID = Path(...),
    session_id: UUID = Path(...),
    user: User = Depends(get_current_user),
    svc: ExternalAdminService = Depends(_svc),
):
    return await svc.get_action_session(project_id, workspace_id, session_id)


@router.post("/action-sessions/{session_id}/approve", response_model=AssistantActionSessionResponse)
async def approve_action_session(
    body: ActionSessionReviewRequest,
    workspace_id: UUID = Depends(require_active_billing_for_workspace_id),
    project_id: UUID = Path(...),
    session_id: UUID = Path(...),
    user: User = Depends(get_current_user),
    svc: ExternalAdminService = Depends(_svc),
):
    return await svc.approve_action_session(project_id, workspace_id, session_id, user.id, note=body.note)


@router.post("/action-sessions/{session_id}/reject", response_model=AssistantActionSessionResponse)
async def reject_action_session(
    body: ActionSessionReviewRequest,
    workspace_id: UUID = Depends(require_active_billing_for_workspace_id),
    project_id: UUID = Path(...),
    session_id: UUID = Path(...),
    user: User = Depends(get_current_user),
    svc: ExternalAdminService = Depends(_svc),
):
    return await svc.reject_action_session(project_id, workspace_id, session_id, user.id, note=body.note)


# ── Consents ──────────────────────────────────────────────────────────────────

@router.get("/consents", response_model=list[AssistantDataConsentResponse])
async def list_consents(
    workspace_id: UUID = Depends(require_active_billing_for_workspace_id),
    project_id: UUID = Path(...),
    user: User = Depends(get_current_user),
    svc: ExternalAdminService = Depends(_svc),
):
    return await svc.list_consents(project_id, workspace_id)


@router.post("/consents", response_model=AssistantDataConsentResponse, status_code=201)
async def grant_consent(
    body: GrantConsentRequest,
    workspace_id: UUID = Depends(require_active_billing_for_workspace_id),
    project_id: UUID = Path(...),
    user: User = Depends(get_current_user),
    svc: ExternalAdminService = Depends(_svc),
):
    return await svc.grant_consent(project_id, workspace_id, body.consent_type, user.id, notes=body.notes)


@router.delete("/consents/{consent_type}", status_code=204)
async def revoke_consent(
    workspace_id: UUID = Depends(require_active_billing_for_workspace_id),
    project_id: UUID = Path(...),
    consent_type: str = Path(...),
    user: User = Depends(get_current_user),
    svc: ExternalAdminService = Depends(_svc),
):
    await svc.revoke_consent(project_id, workspace_id, consent_type, user.id)


@router.post("/wordpress/connect-code", response_model=WordPressConnectCodeResponse)
async def issue_wordpress_connect_code(
    body: WordPressConnectCodeRequest,
    workspace_id: UUID = Depends(require_active_billing_for_workspace_id),
    project_id: UUID = Path(...),
    user: User = Depends(get_current_user),
    svc: ExternalAdminService = Depends(_svc),
    redis=Depends(get_redis),
):
    return await svc.issue_wordpress_connect_code(
        project_id, workspace_id, body.site_url, redis, return_url=body.return_url
    )


# ── Plugin-facing: WordPress ──────────────────────────────────────────────────

@workspace_router.post("/wordpress/connect-project", response_model=WordPressConnectProjectResponse)
async def create_wordpress_connect_project(
    body: WordPressConnectProjectRequest,
    workspace_id: UUID = Path(...),
    user: User = Depends(get_current_user),
    project_service: ProjectService = Depends(get_project_service),
    svc: ExternalAdminService = Depends(_svc),
    redis=Depends(get_redis),
):
    """Create (or reuse) a WordPress AI Assist project during onboarding, before billing activation.

    Reuses an existing project when the same site reconnects, so repeated connects
    from one WP site do not pile up duplicate projects in the workspace.
    """
    if body.return_url:
        # Fail before creating a project for a return address that is not this site's wp-admin.
        validate_wordpress_return_url(body.site_url, body.return_url)
    existing_project_id = await svc.find_existing_wordpress_project_id(workspace_id, body.site_url)
    if existing_project_id is not None:
        project_id = existing_project_id
    else:
        project = await project_service.create_project(
            workspace_id=workspace_id,
            name=body.name,
            project_type="ai_assist_wordpress",
            description=body.description,
            user_id=user.id,
        )
        project_id = project.id

    code = await svc.issue_wordpress_connect_code(
        project_id, workspace_id, body.site_url, redis, return_url=body.return_url
    )
    return WordPressConnectProjectResponse(
        ok=code.ok,
        project_id=str(project_id),
        installation_id=code.installation_id,
        connect_code=code.connect_code,
        app_url=code.app_url,
        core_api_url=code.core_api_url,
    )


@workspace_router.post("/shopify/connect-project", response_model=ShopifyConnectProjectResponse)
async def create_shopify_connect_project(
    body: ShopifyConnectProjectRequest,
    workspace_id: UUID = Path(...),
    user: User = Depends(get_current_user),
    project_service: ProjectService = Depends(get_project_service),
    svc: ExternalAdminService = Depends(_svc),
):
    """Create (or reuse) a Shopify AI Assist project during onboarding, before billing activation.

    Reuses the workspace's existing project for the shop, so reinstalling an uninstalled store does
    not pile up projects and installations; refuses when another workspace has the shop connected.
    """
    existing_project_id = await svc.resolve_shopify_connect_target(workspace_id, body.shop_domain)
    if existing_project_id is not None:
        project_id = existing_project_id
    else:
        project = await project_service.create_project(
            workspace_id=workspace_id,
            name=body.name,
            project_type="ai_assist_shopify",
            description=body.description,
            user_id=user.id,
        )
        project_id = project.id
    return await svc.create_shopify_pending_installation(project_id, workspace_id, body.shop_domain)


@plugin_router.post("/wordpress/connect/start", response_model=WordPressHostedUrlResponse)
async def wordpress_connect_start(
    body: WordPressConnectStartRequest,
    _edge: None = Depends(_require_wp_edge_new),
    svc: ExternalAdminService = Depends(_svc),
):
    return await svc.build_wordpress_connect_start_url(body)


@plugin_router.post("/wordpress/connect/complete", response_model=WordPressConnectCompleteResponse)
async def wordpress_connect_complete(
    body: WordPressConnectCompleteRequest,
    _edge: None = Depends(_require_wp_edge_new),
    svc: ExternalAdminService = Depends(_svc),
    redis=Depends(get_redis),
):
    return await svc.handle_wordpress_connect_complete(body, redis)


@plugin_router.post("/wordpress/embed/exchange", response_model=WordPressEmbedExchangeResponse)
async def wordpress_embed_exchange(
    body: WordPressEmbedExchangeRequest,
    svc: ExternalAdminService = Depends(_svc),
):
    return await svc.handle_wordpress_embed_exchange(body)


@plugin_router.post("/wordpress/embed-url", response_model=WordPressHostedUrlResponse)
async def wordpress_embed_url(
    body: WordPressHostedUrlRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_wp_gateway),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.build_wordpress_embed_url(token, body)


@plugin_router.post("/wordpress/critical-flow", response_model=WordPressHostedUrlResponse)
async def wordpress_critical_flow(
    body: WordPressHostedUrlRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_wp_gateway),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.build_wordpress_critical_flow_url(token, body)


@plugin_router.post("/wordpress/health", response_model=WordPressHeartbeatResponse)
async def wordpress_heartbeat(
    body: WordPressHeartbeatRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_wp_gateway),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_wordpress_heartbeat(token, body)


@plugin_router.post("/wordpress/chat/session/open", response_model=WordPressChatSessionOpenResponse)
async def wordpress_chat_session_open(
    body: WordPressChatSessionOpenRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_wp_gateway),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_wordpress_chat_session_open(token, body)


@plugin_router.post("/wordpress/chat/messages/send", response_model=WordPressChatMessageSendResponse)
async def wordpress_chat_message_send(
    body: WordPressChatMessageSendRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_wp_gateway),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_wordpress_chat_message_send(token, body)


@plugin_router.post("/wordpress/chat/messages/list", response_model=WordPressChatMessageListResponse)
async def wordpress_chat_message_list(
    body: WordPressChatMessageListRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_wp_gateway),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_wordpress_chat_message_list(token, body)


@plugin_router.post("/wordpress/chat/jobs/poll", response_model=WordPressChatJobPollResponse)
async def wordpress_chat_job_poll(
    body: WordPressChatJobPollRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_wp_gateway),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_wordpress_chat_job_poll(token, body)


@plugin_router.post("/wordpress/site-understanding/sync", response_model=WordPressSiteUnderstandingSyncResponse)
async def wordpress_site_understanding_sync(
    body: WordPressSiteUnderstandingSyncRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_wp_gateway),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_wordpress_site_understanding_start(token, body)


@plugin_router.post("/wordpress/site-understanding/status", response_model=WordPressSiteUnderstandingStatusResponse)
async def wordpress_site_understanding_status(
    body: WordPressSiteUnderstandingStatusRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_wp_gateway),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_wordpress_site_understanding_status(token, body)


@plugin_router.post("/wordpress/companion/install", response_model=CompanionInstallResponse)
async def companion_install(
    body: CompanionInstallRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_wp_gateway),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_wordpress_companion_install(token, body)


@plugin_router.get("/wordpress/companion/packages/{version}/download")
async def companion_package_download(
    version: str = Path(..., min_length=1, max_length=40),
    _gateway: None = Depends(_require_wp_gateway),
):
    if not get_app_settings().wp_companion_install_enabled:
        raise ForbiddenError("companion_install_disabled")
    package = resolve_wordpress_companion_package(version)
    if not package:
        raise ForbiddenError("companion_package_not_found")
    return FileResponse(
        package.path,
        media_type="application/gzip",
        filename="p2hs-companion.tar.gz",
    )


@plugin_router.post(
    "/wordpress/apply/poll",
    response_model=WordPressCompanionMutationPollResponse,
)
async def wordpress_apply_poll(
    body: WordPressCompanionMutationPollRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_wp_gateway),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_wordpress_apply_poll(token, body)


@plugin_router.post(
    "/wordpress/apply/report",
    response_model=WordPressCompanionMutationReportResponse,
)
async def wordpress_apply_report(
    body: WordPressCompanionMutationReportRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_wp_gateway),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_wordpress_apply_report(token, body)


@plugin_router.post(
    "/wordpress/companion/mutations/poll",
    response_model=WordPressCompanionMutationPollResponse,
)
async def companion_mutation_poll(
    body: WordPressCompanionMutationPollRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_wp_gateway),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_companion_mutation_poll(token, body)


@plugin_router.post(
    "/wordpress/companion/mutations/report",
    response_model=WordPressCompanionMutationReportResponse,
)
async def companion_mutation_report(
    body: WordPressCompanionMutationReportRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_wp_gateway),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_companion_mutation_report(token, body)


# ── Plugin-facing: Shopify ────────────────────────────────────────────────────

@plugin_router.post("/shopify/oauth/callback", response_model=ShopifyOAuthCallbackResponse)
async def shopify_oauth_callback(
    body: ShopifyOAuthCallbackRequest,
    _gateway: None = Depends(_require_shopify_edge),
    svc: ExternalAdminService = Depends(_svc),
):
    # Forwarded from the BFF Worker after it verified Shopify's HMAC and its own browser-bound
    # state; `state` here is the installation id the Worker recorded. There is no installation
    # credential yet (this call is what mints it), so the control is the Edge attestation, not a
    # bearer. The response carries the new per-installation token.
    return await svc.handle_shopify_oauth_callback(body)


@plugin_router.post("/shopify/health", response_model=ShopifyHeartbeatResponse)
async def shopify_heartbeat(
    body: ShopifyHeartbeatRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_shopify_edge),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_shopify_heartbeat(token, body)


@plugin_router.post("/shopify/chat/session/open", response_model=ShopifyChatSessionOpenResponse)
async def shopify_chat_session_open(
    body: ShopifyChatSessionOpenRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_shopify_edge),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_shopify_chat_session_open(token, body)


@plugin_router.post("/shopify/chat/messages/send", response_model=ShopifyChatMessageSendResponse)
async def shopify_chat_message_send(
    body: ShopifyChatMessageSendRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_shopify_edge),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_shopify_chat_message_send(token, body)


@plugin_router.post("/shopify/chat/messages/list", response_model=ShopifyChatMessageListResponse)
async def shopify_chat_message_list(
    body: ShopifyChatMessageListRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_shopify_edge),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_shopify_chat_message_list(token, body)


@plugin_router.post("/shopify/chat/jobs/poll", response_model=ShopifyChatJobPollResponse)
async def shopify_chat_job_poll(
    body: ShopifyChatJobPollRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_shopify_edge),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_shopify_chat_job_poll(token, body)


@plugin_router.post("/shopify/site-understanding/sync", response_model=ShopifySiteUnderstandingSyncResponse)
async def shopify_site_understanding_sync(
    body: ShopifySiteUnderstandingSyncRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_shopify_edge),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_shopify_site_understanding_start(token, body)


@plugin_router.post("/shopify/site-understanding/status", response_model=ShopifySiteUnderstandingStatusResponse)
async def shopify_site_understanding_status(
    body: ShopifySiteUnderstandingStatusRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_shopify_edge),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_shopify_site_understanding_status(token, body)


@plugin_router.post("/shopify/apply/poll", response_model=ShopifyApplyPollResponse)
async def shopify_apply_poll(
    body: ShopifyApplyPollRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_shopify_edge),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_shopify_apply_poll(token, body)


@plugin_router.post("/shopify/apply/report", response_model=ShopifyApplyReportResponse)
async def shopify_apply_report(
    body: ShopifyApplyReportRequest,
    authorization: str = Header(...),
    _gateway: None = Depends(_require_shopify_edge),
    svc: ExternalAdminService = Depends(_svc),
):
    token = _extract_bearer(authorization)
    return await svc.handle_shopify_apply_report(token, body)



@plugin_router.post("/shopify/webhooks")
async def shopify_webhook(
    body: ShopifyWebhookEnvelope,
    _gateway: None = Depends(_require_shopify_edge),
    svc: ExternalAdminService = Depends(_svc),
):
    # Shop-level and HMAC-verified by the BFF before it gets here; identified by shop domain, not
    # by an installation credential (uninstall and erasure arrive after the credential is gone).
    return await svc.handle_shopify_webhook(body)

# ── Helpers ───────────────────────────────────────────────────────────────────

def _extract_bearer(authorization: str) -> str:
    if not authorization.startswith("Bearer "):
        raise ForbiddenError("invalid_authorization_header")
    token = authorization[len("Bearer "):]
    if not token:
        raise ForbiddenError("missing_bearer_token")
    return token
