"""WP chat AI response generation via the configured Core AI provider.

Routes through get_ai_provider() so provider selection (Claude / OpenAI / Cursor)
uses the same env-var config (AI_PROVIDER, ANTHROPIC_API_KEY, OPENAI_API_KEY) as
all other Core AI operations — no separate keys or code paths.

Cursor is repo-based and cannot answer API-only chat: it falls back to
ClaudeCodeProvider (API mode only) then OpenAIProvider.

Provider response shapes:
  Claude (text)  → result.plan["document"] is the full markdown/text.
                   Mutation plans are embedded as a fenced ```json block with a
                   "p2hs_mutation_plan" key that the parser extracts.
  OpenAI (JSON)  → result.plan is a parsed dict. The system prompt instructs
                   the model to return {"response_type", "content", "mutation_plan"}.
"""
from __future__ import annotations

import json
import re
from typing import Any, Optional

import structlog

from src.config import AIProvider, get_settings
from src.integrations.ai_provider import CodegenResult, get_ai_provider

logger = structlog.get_logger()

# Run-class constants mirrored from models to avoid circular import.
_RC_WP_DATA = "wp_data_mutation"
_RC_COMPANION = "companion_update"
_RC_HYBRID = "hybrid"
_RC_READ = "read_report"

_ALLOWED_ACTIONS = [
    "wordpress.post.create",
    "wordpress.post.update",
    "wordpress.content.bulk_replace",
    "wordpress.option.update",
    "wordpress.media.upload",
]

# Matches a fenced ```json block that contains a "p2hs_mutation_plan" key.
_PLAN_BLOCK_RE = re.compile(
    r"```json\s*(\{[\s\S]*?\"p2hs_mutation_plan\"[\s\S]*?\})\s*```",
    re.DOTALL,
)


async def call_wp_chat_ai(
    context: dict[str, Any],
) -> tuple[str, Optional[dict], str]:
    """Call the configured AI provider for a WP chat turn.

    Returns (assistant_content, mutation_plan | None, run_class).
    Callers should not catch exceptions from here — they are handled internally
    and surfaced as a friendly error message.
    """
    settings = get_settings()
    provider = _resolve_provider(settings)
    is_json_provider = provider.provider_name == "openai"

    system_prompt = (
        _build_json_system_prompt(context) if is_json_provider
        else _build_text_system_prompt(context)
    )
    user_prompt = _flatten_history(context)

    if not user_prompt:
        return "Please send a message to get started.", None, _RC_READ

    try:
        result = await provider.generate_plan(system_prompt, user_prompt)
    except Exception as exc:
        logger.exception("wp_chat_ai_provider_error", provider=provider.provider_name, error=str(exc))
        return "I encountered an error. Please try again.", None, _RC_READ

    if result.provider_invocation_failed:
        logger.warning("wp_chat_ai_provider_invocation_failed", provider=provider.provider_name)
        return "The AI provider could not respond. Please try again.", None, _RC_READ

    if is_json_provider:
        return _parse_openai(result)
    return _parse_text(result)


# ── Provider resolution ────────────────────────────────────────────────────────

def _resolve_provider(settings):
    """Return an API-capable provider for chat.

    Cursor is a repo-based CLI tool; for chat (no git workspace) fall back to
    Claude API then OpenAI API using the same keys from settings.
    """
    if settings.ai_provider != AIProvider.CURSOR:
        return get_ai_provider()

    # Cursor fallback: prefer Anthropic SDK, then OpenAI.
    if settings.anthropic_api_key:
        from src.integrations.claude_code import ClaudeCodeProvider
        logger.info("wp_chat_ai_cursor_fallback", fallback="claude")
        return ClaudeCodeProvider()

    from src.integrations.openai_client import OpenAIProvider
    logger.info("wp_chat_ai_cursor_fallback", fallback="openai")
    return OpenAIProvider()


# ── Prompt builders ────────────────────────────────────────────────────────────

def _build_text_system_prompt(context: dict[str, Any]) -> str:
    """System prompt for text-response providers (Claude, Cursor-fallback-to-Claude)."""
    preamble = _site_preamble(context)
    return f"""{preamble}

## Mutation plans
When the user explicitly wants to change something on their site, include a mutation plan in
your response using this exact fenced JSON block (the visible text before/after is your
explanation to the user):

```json
{{
  "p2hs_mutation_plan": {{
    "summary": "short summary of what will change",
    "run_class": "{_RC_WP_DATA}",
    "user_message": "The approval message shown to the site owner. Be specific about what changes.",
    "operations": [
      {{
        "action": "wordpress.post.update",
        "description": "human-readable description",
        "payload": {{"post_id": 123, "title": "New title", "content": "..."}}
      }}
    ]
  }}
}}
```

Allowed run_class values: {_RC_WP_DATA} | {_RC_COMPANION} | {_RC_HYBRID}
Allowed actions: {", ".join(_ALLOWED_ACTIONS)}

For read-only questions, reports, or explanations — respond in plain text. No JSON block."""


def _build_json_system_prompt(context: dict[str, Any]) -> str:
    """System prompt for JSON-response providers (OpenAI with response_format=json_object)."""
    preamble = _site_preamble(context)
    return f"""{preamble}

Always respond with a JSON object in this exact format:
{{
  "response_type": "read_report",
  "content": "Your full response to the user"
}}

OR when the user wants to change something on their site:
{{
  "response_type": "mutation",
  "content": "Your explanation and approval request shown to the user",
  "mutation_plan": {{
    "summary": "short summary",
    "run_class": "{_RC_WP_DATA}",
    "user_message": "The message shown to the site owner requesting approval",
    "operations": [
      {{
        "action": "wordpress.post.update",
        "description": "human-readable description",
        "payload": {{"post_id": 123}}
      }}
    ]
  }}
}}

Allowed run_class values: {_RC_WP_DATA} | {_RC_COMPANION} | {_RC_HYBRID}
Allowed actions: {", ".join(_ALLOWED_ACTIONS)}"""


def _site_preamble(context: dict[str, Any]) -> str:
    install = context.get("installation") or {}
    su = context.get("site_understanding") or {}
    su_summary = su.get("summary") or {}
    settings_ctx = context.get("assistant_settings") or {}
    platform = str(context.get("platform") or "wordpress")

    site_url = install.get("shop_domain") or install.get("site_url") or "unknown"
    su_status = su.get("status") or "unknown"
    approval_required = settings_ctx.get("action_approval_required", True)

    inventory_lines = ""
    if su_summary.get("plugin_count") is not None:
        inventory_lines += f"\n- Plugins: {su_summary['plugin_count']}"
    if su_summary.get("entity_count") is not None:
        inventory_lines += f"\n- Content entities indexed: {su_summary['entity_count']}"
    if su_summary.get("product_count") is not None:
        inventory_lines += f"\n- Products indexed: {su_summary['product_count']}"
    if su_summary.get("theme_asset_count") is not None:
        inventory_lines += f"\n- Theme assets indexed: {su_summary['theme_asset_count']}"

    approval_note = (
        "All mutations require the site owner's approval before execution."
        if approval_required
        else "Mutations may execute immediately on approval (approval-gate disabled for this site)."
    )

    platform_name = "Shopify store" if platform == "shopify" else "WordPress site"
    mutation_target = (
        "Use Shopify Admin API action names such as shopify.product.update, shopify.page.update, or shopify.theme_asset.update."
        if platform == "shopify"
        else "Use WordPress action names such as wordpress.post.update or wordpress.option.update."
    )

    return f"""You are P2HS AI, a {platform_name} assistant for {site_url}.

## Site context
- Target: {site_url}
- Site Understanding: {su_status}{inventory_lines}

## Rules
- NEVER include passwords, secret keys, auth salts, database credentials, or API tokens in any plan.
- NEVER modify wp-config.php secrets or sensitive option keys (auth_key, auth_salt, etc.).
- For read-only questions, answer in plain text — no mutation plan needed.
- For mutations, explain what will change before proposing the plan.
- {mutation_target}
- {approval_note}"""


# ── Conversation formatting ────────────────────────────────────────────────────

def _flatten_history(context: dict[str, Any]) -> str:
    """Format conversation history as a single user_prompt string.

    generate_plan() takes a single string, not a messages list. Format history
    so all providers get the same context regardless of their native API shape.
    """
    history = context.get("conversation_history") or []
    if not history:
        return ""

    lines: list[str] = []
    for msg in history[:-1]:  # all but the last (current) user message
        role = msg.get("role", "")
        content = str(msg.get("content") or "").strip()
        if role == "user":
            lines.append(f"[User]: {content}")
        elif role == "assistant":
            lines.append(f"[P2HS AI]: {content}")

    current = history[-1] if history else {}
    current_content = str(current.get("content") or "").strip()

    if lines:
        prior = "\n".join(lines)
        return f"Prior conversation:\n{prior}\n\nLatest user message: {current_content}"
    return current_content


# ── Response parsers ───────────────────────────────────────────────────────────

def _parse_openai(result: CodegenResult) -> tuple[str, Optional[dict], str]:
    plan_dict = result.plan or {}
    content = str(plan_dict.get("content") or result.summary or "").strip()
    response_type = str(plan_dict.get("response_type") or "read_report")
    mutation_plan = plan_dict.get("mutation_plan")

    if not content:
        content = "I couldn't generate a response. Please try again."

    if response_type == "mutation" and isinstance(mutation_plan, dict):
        ops = mutation_plan.get("operations")
        if isinstance(ops, list) and ops:
            run_class = str(mutation_plan.get("run_class") or _RC_WP_DATA)
            user_message = str(mutation_plan.get("user_message") or content)
            return user_message, mutation_plan, run_class

    return content, None, _RC_READ


def _parse_text(result: CodegenResult) -> tuple[str, Optional[dict], str]:
    plan_dict = result.plan or {}
    full_text = str(plan_dict.get("document") or result.summary or "").strip()

    match = _PLAN_BLOCK_RE.search(full_text)
    if match:
        try:
            block = json.loads(match.group(1))
            mutation_plan = block.get("p2hs_mutation_plan")
            if isinstance(mutation_plan, dict):
                ops = mutation_plan.get("operations")
                if isinstance(ops, list) and ops:
                    visible = _PLAN_BLOCK_RE.sub("", full_text).strip()
                    user_message = str(mutation_plan.get("user_message") or visible or "Plan prepared.")
                    run_class = str(mutation_plan.get("run_class") or _RC_WP_DATA)
                    return user_message, mutation_plan, run_class
        except json.JSONDecodeError:
            logger.warning("wp_chat_ai_plan_json_parse_failed")

    content = full_text or "I couldn't generate a response. Please try again."
    return content, None, _RC_READ
