"""Shopify App domain - business logic."""
from __future__ import annotations

import secrets
from datetime import datetime, timedelta, timezone
from typing import Optional
from uuid import UUID

from jose import jwt

from src.config import get_settings
from src.core.exceptions import NotFoundError, ValidationError
from src.core.security.encryption import decrypt_value, encrypt_value
from src.domain.shopify_apps.models import (
    FUNCTION_STATUS_ACTIVE,
    TRIGGER_TYPE_CRON,
    TRIGGER_TYPE_WEBHOOK,
    ShopifyAppConfig,
    ShopifyAppFunction,
)
from src.domain.shopify_apps.repository import ShopifyAppConfigRepository, ShopifyAppFunctionRepository
from src.domain.shopify_apps.schemas import (
    ShopifyAppConfigResponse,
    ShopifyAppFunctionCreate,
    ShopifyAppFunctionDetailResponse,
    ShopifyAppFunctionResponse,
    ShopifyAppFunctionUpdate,
    ShopifyAppInstallInstructions,
    ShopifyAppPreviewTokenResponse,
)

PREVIEW_TOKEN_TTL_MINUTES = 10
SHOPIFY_APP_PROJECT_TYPE = "shopify_app"


def worker_script_name(project_ref: str) -> str:
    """Deterministic CF Worker script name from project_ref."""
    slug = "".join(c if c.isalnum() or c == "-" else "-" for c in project_ref.lower())
    return f"p2hs-shopify-{slug}"[:63]


class ShopifyAppService:
    def __init__(
        self,
        config_repo: ShopifyAppConfigRepository,
        function_repo: ShopifyAppFunctionRepository,
    ) -> None:
        self._config_repo = config_repo
        self._function_repo = function_repo

    # ── Config ────────────────────────────────────────────────────────────────

    async def get_or_create_config(self, project_id: UUID, project_ref: str) -> ShopifyAppConfigResponse:
        cfg = await self._config_repo.get(project_id)
        if cfg is None:
            preview_secret = secrets.token_urlsafe(32)
            w_name = worker_script_name(project_ref)
            cfg = await self._config_repo.upsert(
                project_id,
                preview_jwt_secret_enc=encrypt_value(preview_secret),
                worker_name=w_name,
            )
        return _map_config(cfg)

    async def update_config(
        self,
        project_id: UUID,
        project_ref: str,
        *,
        shop_domain: Optional[str] = None,
        access_token: Optional[str] = None,
        webhook_secret: Optional[str] = None,
    ) -> ShopifyAppConfigResponse:
        cfg = await self._config_repo.get(project_id)
        updates: dict = {}
        if cfg is None:
            preview_secret = secrets.token_urlsafe(32)
            w_name = worker_script_name(project_ref)
            updates["preview_jwt_secret_enc"] = encrypt_value(preview_secret)
            updates["worker_name"] = w_name

        if shop_domain is not None:
            updates["shop_domain"] = shop_domain.strip().lower().rstrip("/")
        if access_token is not None:
            updates["access_token_enc"] = encrypt_value(access_token) if access_token else None
        if webhook_secret is not None:
            updates["webhook_secret_enc"] = encrypt_value(webhook_secret) if webhook_secret else None

        cfg = await self._config_repo.upsert(project_id, **updates)
        return _map_config(cfg)

    async def get_preview_token(self, project_id: UUID, project_ref: str) -> ShopifyAppPreviewTokenResponse:
        cfg = await self._config_repo.get(project_id)
        if cfg is None or not cfg.preview_jwt_secret_enc:
            # Auto-provision
            await self.get_or_create_config(project_id, project_ref)
            cfg = await self._config_repo.get(project_id)

        secret = decrypt_value(cfg.preview_jwt_secret_enc)
        now = datetime.now(timezone.utc)
        exp = now + timedelta(minutes=PREVIEW_TOKEN_TTL_MINUTES)
        payload = {
            "sub": str(project_id),
            "type": "shopify_app_preview",
            "iat": int(now.timestamp()),
            "exp": int(exp.timestamp()),
        }
        token = jwt.encode(payload, secret, algorithm="HS256")
        return ShopifyAppPreviewTokenResponse(
            token=token,
            expires_in=PREVIEW_TOKEN_TTL_MINUTES * 60,
            worker_url=cfg.worker_url,
        )

    async def get_install_instructions(self, project_id: UUID) -> ShopifyAppInstallInstructions:
        cfg = await self._config_repo.get(project_id)
        functions = await self._function_repo.list_for_project(project_id)
        worker_url = cfg.worker_url if cfg else None
        shop_domain = cfg.shop_domain if cfg else None
        worker_name = cfg.worker_name if cfg else None

        webhook_endpoints = [
            {"slug": fn.slug, "topic": fn.webhook_topic, "url": f"{worker_url}/webhooks/{fn.slug}"}
            for fn in functions
            if fn.trigger_type == TRIGGER_TYPE_WEBHOOK and fn.status == FUNCTION_STATUS_ACTIVE
        ]

        app_url = worker_url or "<your-worker-url>"
        steps = [
            "1. Open **Store Configuration** in this dashboard",
            "2. Enter your shop domain (e.g. `mystore.myshopify.com`)",
            "3. Click **Connect to Shopify store** and approve access in Shopify Admin",
            f"4. After connecting, your Worker URL is: {app_url}/",
            "5. Install the embedded app from Shopify Admin → Apps if you use the Live App tab",
        ]
        if webhook_endpoints:
            steps.append("10. Register the webhook endpoints below in Shopify Admin → Notifications → Webhooks")

        return ShopifyAppInstallInstructions(
            worker_url=worker_url,
            worker_name=worker_name,
            shop_domain=shop_domain,
            steps=steps,
            webhook_endpoints=webhook_endpoints,
        )

    # ── Functions ─────────────────────────────────────────────────────────────

    async def list_functions(self, project_id: UUID) -> list[ShopifyAppFunctionResponse]:
        fns = await self._function_repo.list_for_project(project_id)
        return [_map_function(fn) for fn in fns]

    async def create_function(
        self, project_id: UUID, data: ShopifyAppFunctionCreate
    ) -> ShopifyAppFunctionDetailResponse:
        if data.trigger_type == TRIGGER_TYPE_CRON and not data.cron_schedule:
            raise ValidationError("cron_schedule is required for cron functions")
        if data.trigger_type == TRIGGER_TYPE_WEBHOOK and not data.webhook_topic:
            raise ValidationError("webhook_topic is required for webhook functions")

        existing = await self._function_repo.get_by_slug(project_id, data.slug)
        if existing:
            raise ValidationError(f"A function with slug '{data.slug}' already exists in this project")

        fn = await self._function_repo.create(
            project_id=project_id,
            name=data.name,
            slug=data.slug,
            trigger_type=data.trigger_type,
            description=data.description,
            cron_schedule=data.cron_schedule,
            webhook_topic=data.webhook_topic,
            code_ts=data.code_ts,
        )
        return _map_function_detail(fn)

    async def get_function(self, project_id: UUID, function_id: UUID) -> ShopifyAppFunctionDetailResponse:
        fn = await self._function_repo.get_by_id(function_id, project_id)
        if not fn:
            raise NotFoundError("Function not found")
        return _map_function_detail(fn)

    async def update_function(
        self, project_id: UUID, function_id: UUID, data: ShopifyAppFunctionUpdate
    ) -> ShopifyAppFunctionDetailResponse:
        fn = await self._function_repo.get_by_id(function_id, project_id)
        if not fn:
            raise NotFoundError("Function not found")

        updates: dict = {}
        if data.name is not None:
            updates["name"] = data.name
        if data.description is not None:
            updates["description"] = data.description
        if data.trigger_type is not None:
            updates["trigger_type"] = data.trigger_type
        if data.cron_schedule is not None:
            updates["cron_schedule"] = data.cron_schedule
        if data.webhook_topic is not None:
            updates["webhook_topic"] = data.webhook_topic
        if data.code_ts is not None:
            updates["code_ts"] = data.code_ts
        if data.status is not None:
            if data.status not in ("draft", "active", "disabled"):
                raise ValidationError("status must be draft, active, or disabled")
            updates["status"] = data.status

        fn = await self._function_repo.update(fn, **updates)
        return _map_function_detail(fn)

    async def delete_function(self, project_id: UUID, function_id: UUID) -> None:
        fn = await self._function_repo.get_by_id(function_id, project_id)
        if not fn:
            raise NotFoundError("Function not found")
        await self._function_repo.delete(fn)


# ── Mappers ────────────────────────────────────────────────────────────────────

def _map_config(cfg: ShopifyAppConfig) -> ShopifyAppConfigResponse:
    return ShopifyAppConfigResponse(
        project_id=cfg.project_id,
        shop_domain=cfg.shop_domain,
        has_api_key=bool(cfg.shopify_api_key_enc),
        has_api_secret=bool(cfg.shopify_api_secret_enc),
        has_access_token=bool(cfg.access_token_enc),
        has_webhook_secret=bool(cfg.webhook_secret_enc),
        worker_name=cfg.worker_name,
        worker_url=cfg.worker_url,
        updated_at=cfg.updated_at,
    )


def _map_function(fn: ShopifyAppFunction) -> ShopifyAppFunctionResponse:
    return ShopifyAppFunctionResponse(
        id=fn.id,
        project_id=fn.project_id,
        name=fn.name,
        slug=fn.slug,
        description=fn.description,
        trigger_type=fn.trigger_type,
        cron_schedule=fn.cron_schedule,
        webhook_topic=fn.webhook_topic,
        has_code=bool(fn.code_ts),
        status=fn.status,
        last_run_at=fn.last_run_at,
        last_run_status=fn.last_run_status,
        created_at=fn.created_at,
        updated_at=fn.updated_at,
    )


def _map_function_detail(fn: ShopifyAppFunction) -> ShopifyAppFunctionDetailResponse:
    return ShopifyAppFunctionDetailResponse(
        id=fn.id,
        project_id=fn.project_id,
        name=fn.name,
        slug=fn.slug,
        description=fn.description,
        trigger_type=fn.trigger_type,
        cron_schedule=fn.cron_schedule,
        webhook_topic=fn.webhook_topic,
        has_code=bool(fn.code_ts),
        code_ts=fn.code_ts,
        status=fn.status,
        last_run_at=fn.last_run_at,
        last_run_status=fn.last_run_status,
        last_run_log=fn.last_run_log,
        created_at=fn.created_at,
        updated_at=fn.updated_at,
    )
