import base64
import logging
import os
from typing import Iterator

from castor.prompt_cache import CacheStats, build_cached_system_prompt

from .base import BaseProvider, Thought

logger = logging.getLogger("OpenCastor.Anthropic")


class AnthropicProvider(BaseProvider):
    """Anthropic Claude adapter. Optimized for complex reasoning and safety."""

    # Default to the latest Claude model when none specified in config
    DEFAULT_MODEL = "claude-opus-4-6"

    # Claude setup-token prefix (generated by `claude setup-token`)
    SETUP_TOKEN_PREFIX = "sk-ant-oat01-"

    def __init__(self, config):
        # Apply default model before super().__init__ reads it
        if not config.get("model") or config.get("model") == "default-model":
            config["model"] = self.DEFAULT_MODEL
        super().__init__(config)
        import anthropic

        # Resolution order:
        # 1. OpenCastor's own stored token (~/.opencastor/anthropic-token)
        #    — This takes priority because it's explicitly set via `castor login`
        # 2. ANTHROPIC_API_KEY env var
        # 3. config api_key
        #
        # NOTE: We do NOT read from ~/.claude/.credentials.json — that belongs
        # to Claude CLI / OpenClaw. Reading it risks token invalidation (the
        # "token sink" problem where refreshing one client's token kills another's).
        api_key = self._read_stored_token()

        if not api_key:
            api_key = os.getenv("ANTHROPIC_API_KEY") or config.get("api_key")

        if not api_key:
            raise ValueError(
                "No Anthropic credentials found. Options:\n"
                "  1. Run 'castor login anthropic' to generate a setup-token\n"
                "     (uses your Claude Max/Pro subscription — no per-token billing)\n"
                "  2. Set ANTHROPIC_API_KEY to an API key from console.anthropic.com\n"
                "  3. Run 'castor wizard' to configure interactively"
            )

        if api_key.startswith(self.SETUP_TOKEN_PREFIX):
            # OAuth setup-token — route through Claude CLI (handles OAuth
            # token exchange internally). This mirrors how OpenClaw uses
            # Claude Max/Pro subscriptions.
            from castor.claude_proxy import ClaudeOAuthClient

            logger.info("Using Claude subscription via CLI (OAuth token)")
            self._use_cli = True
            self._cli_client = ClaudeOAuthClient(api_key)
            self.client = None
        else:
            logger.info("Using Anthropic API key")
            self._use_cli = False
            self.client = anthropic.Anthropic(api_key=api_key)

        # Build once — same blocks every call = cache hits
        # rcan_config is optional; passed in by TieredBrain when available
        self._cached_system_blocks = build_cached_system_prompt(
            self.system_prompt,
            config.get("rcan_config"),
        )
        self._cache_stats = CacheStats()

    # Path for OpenCastor's own token store (separate from Claude CLI / OpenClaw)
    TOKEN_PATH = os.path.expanduser("~/.opencastor/anthropic-token")

    @classmethod
    def _read_stored_token(cls):
        """Read OpenCastor's own stored Anthropic token.

        Stored at ~/.opencastor/anthropic-token by 'castor login anthropic'.
        This is intentionally separate from ~/.claude/.credentials.json
        to avoid the token sink problem (invalidating OpenClaw's token).
        """
        try:
            if os.path.exists(cls.TOKEN_PATH):
                with open(cls.TOKEN_PATH) as f:
                    token = f.read().strip()
                if token:
                    logger.debug("Read Anthropic token from %s", cls.TOKEN_PATH)
                    return token
        except Exception as e:
            logger.debug(f"Could not read stored token: {e}")
        return None

    @classmethod
    def save_token(cls, token: str) -> str:
        """Save an Anthropic token to OpenCastor's token store.

        Returns the path where the token was saved.
        """
        token_dir = os.path.dirname(cls.TOKEN_PATH)
        os.makedirs(token_dir, mode=0o700, exist_ok=True)
        with open(cls.TOKEN_PATH, "w") as f:
            f.write(token)
        os.chmod(cls.TOKEN_PATH, 0o600)
        return cls.TOKEN_PATH

    def think(
        self,
        image_bytes: bytes,
        instruction: str,
        surface: str = "whatsapp",
    ) -> Thought:
        safety_block = self._check_instruction_safety(instruction)
        if safety_block is not None:
            return safety_block

        # Route through CLI if using OAuth token
        if getattr(self, "_use_cli", False):
            return self._think_via_cli(instruction)

        # Determine if we have a real camera frame
        is_blank = not image_bytes or image_bytes == b"\x00" * len(image_bytes)

        # Use the conversational messaging prompt when there's no live camera frame
        if is_blank:
            system_prompt = self.build_messaging_prompt(surface=surface)
        else:
            system_prompt = self.system_prompt  # action-JSON vision prompt

        b64_image = base64.b64encode(image_bytes).decode("utf-8") if not is_blank else ""

        # Build message content -- include image only if it has real data
        content = []
        if not is_blank and len(image_bytes) > 100:
            content.append(
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": "image/jpeg",
                        "data": b64_image,
                    },
                }
            )
        content.append({"type": "text", "text": instruction})

        try:
            # Use cached system blocks for vision/action mode; plain string for messaging
            system_arg = self._cached_system_blocks if not is_blank else system_prompt
            response = self.client.messages.create(
                model=self.model_name,
                max_tokens=1024,
                system=system_arg,
                messages=[{"role": "user", "content": content}],
                extra_headers={"anthropic-beta": "prompt-caching-2024-07-31"},
            )
            # Track cache stats
            self._cache_stats.record(response.usage)
            self._cache_stats.alert_if_low(logger=logger)
            # Track runtime stats
            try:
                from castor.runtime_stats import record_api_call

                usage = response.usage
                _tokens_in = getattr(usage, "input_tokens", 0)
                _tokens_out = getattr(usage, "output_tokens", 0)
                record_api_call(
                    tokens_in=_tokens_in,
                    tokens_out=_tokens_out,
                    tokens_cached=getattr(usage, "cache_read_input_tokens", 0),
                    bytes_in=len(instruction.encode()),
                    bytes_out=len(response.content[0].text.encode()) if response.content else 0,
                    model=self.model_name,
                )
            except Exception:
                pass
            try:
                from castor.usage import get_tracker

                get_tracker().log_usage(
                    provider="anthropic",
                    model=self.model_name,
                    prompt_tokens=_tokens_in,
                    completion_tokens=_tokens_out,
                )
            except Exception:
                pass
            text = response.content[0].text
            action = self._clean_json(text)
            return Thought(text, action)
        except Exception as e:
            logger.error(f"Anthropic error: {e}")
            return Thought(f"Error: {e}", None)

    def get_usage_stats(self) -> dict:
        """Return session-level token usage and cache stats."""
        stats: dict = {}
        try:
            from castor.runtime_stats import get_stats

            rs = get_stats()
            stats["prompt_tokens"] = rs.get("tokens_in", 0)
            stats["completion_tokens"] = rs.get("tokens_out", 0)
            stats["total_requests"] = rs.get("api_calls", 0)
        except Exception:
            pass
        if hasattr(self, "_cache_stats"):
            try:
                cs = self._cache_stats
                stats["cache_hits"] = getattr(cs, "hits", 0)
                stats["cache_misses"] = getattr(cs, "misses", 0)
            except Exception:
                pass
        return stats

    def _think_via_cli(self, instruction: str) -> Thought:
        """Call Claude via OAuth CLI client (uses Max/Pro subscription).

        Passes cache_control content blocks when available so the CLI path
        benefits from the same prompt-caching structure as the direct API path.
        """
        system_arg = getattr(self, "_cached_system_blocks", None) or self.system_prompt
        try:
            response = self._cli_client.create_message(
                model=self.model_name,
                system=system_arg,
                messages=[{"role": "user", "content": instruction}],
                max_tokens=1024,
            )
            text = response["content"][0]["text"]
            action = self._clean_json(text)
            try:
                from castor.runtime_stats import record_api_call

                record_api_call(
                    bytes_in=len(instruction.encode()),
                    bytes_out=len(text.encode()),
                    model=self.model_name,
                )
            except Exception:
                pass
            return Thought(text, action)
        except Exception as e:
            logger.error(f"CLI error: {e}")
            return Thought(f"Error: {e}", None)

    def think_stream(
        self,
        image_bytes: bytes,
        instruction: str,
        surface: str = "whatsapp",
    ) -> Iterator[str]:
        """Stream tokens from the Anthropic Claude model.

        Yields individual text chunks as they arrive.
        Falls back to a single-shot think() via the CLI path (which does not
        support streaming yet).
        """
        safety_block = self._check_instruction_safety(instruction)
        if safety_block is not None:
            yield safety_block.raw_text
            return

        # CLI path doesn't support streaming — fall back to non-streaming
        if getattr(self, "_use_cli", False):
            thought = self._think_via_cli(instruction)
            yield thought.raw_text
            return

        is_blank = not image_bytes or image_bytes == b"\x00" * len(image_bytes)
        system_arg = (
            self.build_messaging_prompt(surface=surface) if is_blank else self._cached_system_blocks
        )
        b64_image = base64.b64encode(image_bytes).decode("utf-8") if not is_blank else ""

        content = []
        if not is_blank and len(image_bytes) > 100:
            content.append(
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": "image/jpeg",
                        "data": b64_image,
                    },
                }
            )
        content.append({"type": "text", "text": instruction})

        try:
            with self.client.messages.stream(
                model=self.model_name,
                max_tokens=1024,
                system=system_arg,
                messages=[{"role": "user", "content": content}],
                extra_headers={"anthropic-beta": "prompt-caching-2024-07-31"},
            ) as stream:
                yield from stream.text_stream
        except Exception as e:
            logger.error(f"Anthropic streaming error: {e}")
            yield f"Error: {e}"

    @property
    def cache_stats(self) -> dict:
        """Return current prompt cache statistics as a dict."""
        return self._cache_stats.to_dict()
