"""Execute an approved Shopify mutation plan directly via the Shopify Admin API.

Flow:
  1. Load the approved AssistantActionSession.
  2. Create WpMutationAction rows (one per operation) for audit trail.
  3. Advance session to 'executing'.
  4. Execute each operation against the Shopify Admin API using the stored
     access token — Core owns this credential and applies the mutation itself.
  5. Mark each action completed or failed; advance session to 'applied' or 'failed'.

This is the Shopify counterpart to execute_wp_mutation.py.  The WP model hands
operations to the plugin to PULL because mutations require code running inside
WordPress.  Shopify mutations go through a public API that Core can call
directly, so there is no PULL phase and no BFF involvement in execution.
"""
from __future__ import annotations

import hashlib
import json
from datetime import UTC, datetime
from typing import Any
from uuid import UUID

import httpx
import structlog

from src.core.database import async_session_factory
from src.core.security.encryption import decrypt_value
from src.domain.external_admin.models import (
    ACTION_SESSION_STATUS_APPLIED,
    ACTION_SESSION_STATUS_EXECUTING,
    ACTION_SESSION_STATUS_FAILED,
    WP_MUTATION_ACTION_STATUS_COMPLETED,
    WP_MUTATION_ACTION_STATUS_FAILED as WP_MUTATION_FAILED,
    WP_MUTATION_ACTION_STATUS_PENDING,
    WP_MUTATION_ACTION_STATUS_RUNNING,
    WpMutationAction,
)
from src.domain.external_admin.repository import (
    AssistantActionSessionRepository,
    ExternalAdminInstallationRepository,
    ExternalCredentialRepository,
    WpMutationActionRepository,
)

logger = structlog.get_logger()

_SHOPIFY_API_VERSION = "2024-01"


async def execute_shopify_mutation_plan(ctx: dict, session_id: str) -> dict:
    """Execute an approved Shopify mutation plan via the Admin API."""
    db_factory = ctx.get("db_factory", async_session_factory)
    try:
        action_session_id = UUID(session_id)
    except ValueError:
        logger.warning("execute_shopify_mutation_invalid_session_id", session_id=session_id)
        return {"status": "invalid_session_id"}

    async with db_factory() as db:
        session_repo = AssistantActionSessionRepository(db)
        session = await session_repo.get(action_session_id)
        if not session:
            logger.warning("execute_shopify_mutation_session_not_found", session_id=session_id)
            return {"status": "not_found"}
        if session.status != "approved":
            return {"status": f"not_runnable:{session.status}"}

        installation = await ExternalAdminInstallationRepository(db).get_by_id(
            session.installation_id
        )
        if not installation or not installation.shop_domain:
            return {"status": "installation_not_found"}

        cred = await ExternalCredentialRepository(db).get_active(
            installation.id, "shopify_access_token"
        )
        if not cred or not cred.value_enc:
            logger.warning(
                "execute_shopify_mutation_no_credential",
                session_id=session_id,
                installation_id=str(installation.id),
            )
            return {"status": "shopify_credential_missing"}

        access_token = decrypt_value(cred.value_enc)
        shop_domain = installation.shop_domain

        plan = session.payload or {}
        operations = plan.get("operations") if isinstance(plan.get("operations"), list) else []

        mutation_repo = WpMutationActionRepository(db)
        action_rows: list[WpMutationAction] = []
        for i, op in enumerate(operations):
            row = WpMutationAction(
                action_session_id=session.id,
                installation_id=session.installation_id,
                project_id=session.project_id,
                sequence_number=i,
                action=str(op.get("action") or ""),
                description=str(op.get("description") or ""),
                payload=op.get("payload") or {},
                status=WP_MUTATION_ACTION_STATUS_PENDING,
            )
            await mutation_repo.create(row)
            action_rows.append(row)

        session.status = ACTION_SESSION_STATUS_EXECUTING
        await session_repo.save(session)
        await db.commit()

    if not action_rows:
        async with db_factory() as db:
            session_repo = AssistantActionSessionRepository(db)
            s = await session_repo.get(action_session_id)
            if s:
                s.status = ACTION_SESSION_STATUS_APPLIED
                s.applied_at = datetime.now(UTC)
                await session_repo.save(s)
                await db.commit()
        return {"status": "applied", "operations": 0}

    # Execute operations sequentially; abort on first failure.
    executor = ShopifyMutationExecutor(shop_domain, access_token)
    succeeded = 0
    failed_at: int | None = None

    for action_row in action_rows:
        async with db_factory() as db:
            mutation_repo = WpMutationActionRepository(db)
            row = await mutation_repo.get_by_id(action_row.id)
            if not row:
                continue
            row.status = WP_MUTATION_ACTION_STATUS_RUNNING
            await mutation_repo.save(row)
            await db.commit()

        try:
            result = await executor.execute(action_row.action, action_row.payload)
            ok = result.get("ok", False)
        except Exception as exc:
            ok = False
            result = {"ok": False, "error": str(exc)[:500]}

        async with db_factory() as db:
            mutation_repo = WpMutationActionRepository(db)
            row = await mutation_repo.get_by_id(action_row.id)
            if not row:
                continue
            row.executed_at = datetime.now(UTC)
            row.result_json = result
            row.after_hash = _hash_result(result)
            if ok:
                row.status = WP_MUTATION_ACTION_STATUS_COMPLETED
                succeeded += 1
            else:
                row.status = WP_MUTATION_FAILED
                row.error = str(result.get("error") or "shopify_mutation_failed")[:500]
                failed_at = action_row.sequence_number
            await mutation_repo.save(row)
            await db.commit()

        if not ok:
            break

    # Mark remaining operations skipped on failure.
    if failed_at is not None:
        async with db_factory() as db:
            mutation_repo = WpMutationActionRepository(db)
            await mutation_repo.mark_pending_as_skipped(action_session_id)
            await db.commit()

    final_status = ACTION_SESSION_STATUS_APPLIED if failed_at is None else ACTION_SESSION_STATUS_FAILED
    async with db_factory() as db:
        session_repo = AssistantActionSessionRepository(db)
        s = await session_repo.get(action_session_id)
        if s:
            s.status = final_status
            if final_status == ACTION_SESSION_STATUS_APPLIED:
                s.applied_at = datetime.now(UTC)
            await session_repo.save(s)
            await db.commit()

    logger.info(
        "execute_shopify_mutation_done",
        session_id=session_id,
        operations=len(action_rows),
        succeeded=succeeded,
        status=final_status,
    )
    return {"status": final_status, "operations": len(action_rows), "succeeded": succeeded}


def _hash_result(value: dict) -> str:
    canonical = json.dumps(value, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
    return hashlib.sha256(canonical.encode()).hexdigest()


class ShopifyMutationExecutor:
    """Execute individual Shopify mutation operations via the Admin REST/GraphQL API."""

    def __init__(self, shop_domain: str, access_token: str) -> None:
        self._shop = shop_domain
        self._base = f"https://{shop_domain}/admin/api/{_SHOPIFY_API_VERSION}"
        self._headers = {
            "X-Shopify-Access-Token": access_token,
            "Content-Type": "application/json",
        }
        self._active_theme_id: int | None = None

    async def execute(self, action: str, payload: dict[str, Any]) -> dict[str, Any]:
        dispatch: dict[str, Any] = {
            "shopify_product_update": self._product_update,
            "shopify_product_create": self._product_create,
            "shopify_page_update": self._page_update,
            "shopify_page_create": self._page_create,
            "shopify_collection_update": self._collection_update,
            "shopify_theme_asset_update": self._theme_asset_update,
            "shopify_metafield_set": self._metafield_set,
        }
        handler = dispatch.get(action)
        if handler is None:
            return {"ok": False, "error": f"unsupported_action:{action}"}
        return await handler(payload)

    async def _product_update(self, payload: dict) -> dict:
        product_id = payload.get("id")
        if not product_id:
            return {"ok": False, "error": "missing_product_id"}
        async with httpx.AsyncClient(timeout=30.0) as client:
            resp = await client.put(
                f"{self._base}/products/{product_id}.json",
                headers=self._headers,
                json={"product": _without(payload, "id")},
            )
        return _rest_result(resp, "product")

    async def _product_create(self, payload: dict) -> dict:
        async with httpx.AsyncClient(timeout=30.0) as client:
            resp = await client.post(
                f"{self._base}/products.json",
                headers=self._headers,
                json={"product": payload},
            )
        return _rest_result(resp, "product")

    async def _page_update(self, payload: dict) -> dict:
        page_id = payload.get("id")
        if not page_id:
            return {"ok": False, "error": "missing_page_id"}
        async with httpx.AsyncClient(timeout=20.0) as client:
            resp = await client.put(
                f"{self._base}/pages/{page_id}.json",
                headers=self._headers,
                json={"page": _without(payload, "id")},
            )
        return _rest_result(resp, "page")

    async def _page_create(self, payload: dict) -> dict:
        async with httpx.AsyncClient(timeout=20.0) as client:
            resp = await client.post(
                f"{self._base}/pages.json",
                headers=self._headers,
                json={"page": payload},
            )
        return _rest_result(resp, "page")

    async def _collection_update(self, payload: dict) -> dict:
        coll_id = payload.get("id")
        if not coll_id:
            return {"ok": False, "error": "missing_collection_id"}
        kind = "custom_collections" if payload.get("kind") != "smart" else "smart_collections"
        async with httpx.AsyncClient(timeout=20.0) as client:
            resp = await client.put(
                f"{self._base}/{kind}/{coll_id}.json",
                headers=self._headers,
                json={"custom_collection": _without(payload, "id", "kind")},
            )
        return _rest_result(resp, "custom_collection")

    async def _theme_asset_update(self, payload: dict) -> dict:
        asset_key = payload.get("key")
        if not asset_key:
            return {"ok": False, "error": "missing_asset_key"}
        theme_id = payload.get("theme_id") or await self._resolve_active_theme_id()
        if not theme_id:
            return {"ok": False, "error": "cannot_resolve_active_theme"}
        async with httpx.AsyncClient(timeout=30.0) as client:
            resp = await client.put(
                f"{self._base}/themes/{theme_id}/assets.json",
                headers=self._headers,
                json={"asset": {"key": asset_key, "value": payload.get("value", "")}},
            )
        return _rest_result(resp, "asset")

    async def _metafield_set(self, payload: dict) -> dict:
        metafields = payload.get("metafields")
        if not isinstance(metafields, list) or not metafields:
            return {"ok": False, "error": "missing_metafields_list"}
        query = """
        mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) {
          metafieldsSet(metafields: $metafields) {
            metafields { id namespace key value }
            userErrors { field message }
          }
        }
        """
        async with httpx.AsyncClient(timeout=20.0) as client:
            resp = await client.post(
                f"https://{self._shop}/admin/api/{_SHOPIFY_API_VERSION}/graphql.json",
                headers=self._headers,
                json={"query": query, "variables": {"metafields": metafields}},
            )
        if resp.status_code != 200:
            return {"ok": False, "error": f"http_{resp.status_code}", "body": resp.text[:300]}
        data = resp.json()
        errors = (data.get("data") or {}).get("metafieldsSet", {}).get("userErrors") or []
        if errors:
            return {"ok": False, "error": errors[0].get("message", "metafield_error"), "errors": errors}
        return {"ok": True, "data": data.get("data")}

    async def _resolve_active_theme_id(self) -> int | None:
        if self._active_theme_id:
            return self._active_theme_id
        try:
            async with httpx.AsyncClient(timeout=15.0) as client:
                resp = await client.get(
                    f"{self._base}/themes.json",
                    headers=self._headers,
                )
                if resp.status_code == 200:
                    themes = resp.json().get("themes") or []
                    for t in themes:
                        if t.get("role") == "main":
                            self._active_theme_id = int(t["id"])
                            return self._active_theme_id
        except Exception:
            pass
        return None


def _rest_result(resp: httpx.Response, key: str) -> dict:
    if resp.status_code in (200, 201):
        return {"ok": True, key: resp.json().get(key)}
    body = {}
    try:
        body = resp.json()
    except Exception:
        pass
    errors = body.get("errors") or body.get("error") or f"http_{resp.status_code}"
    return {"ok": False, "error": str(errors)[:500]}


def _without(payload: dict, *keys: str) -> dict:
    return {k: v for k, v in payload.items() if k not in keys}
