"""WriteOpsMixin — remember() and forget() operations."""
from __future__ import annotations

import asyncio
import logging
from typing import Any, Dict, List, Optional

from cortex.capture.wake_pipeline import (
    strip_transport_metadata_conversation as _strip_transport_metadata_conversation,
    filter_internal_content_conversation as _filter_internal_content_conversation,
    run_extraction as _run_extraction,
    _config_int,
)
from cortex.capture.episode_adapter import log_episode_event as _log_episode_event

from .models import RememberResult, ForgetResult, _new_session_id

logger = logging.getLogger(__name__)


class WriteOpsMixin:
    """Mixin providing write-path memory operations: remember() and forget()."""

    async def remember(
        self,
        conversation: Any,
        session_id: Optional[str] = None,
        scope: str = "user",
        owner_id: Optional[str] = None,
        session_date: Optional[str] = None,
        speaker_a: Optional[str] = None,
        speaker_b: Optional[str] = None,
        # Provenance fields (issue #748)
        source_session_id: Optional[str] = None,
        source_message_id: Optional[str] = None,
        source_channel: Optional[str] = None,
        source_type: Optional[str] = None,
        # Entity scoping (#923)
        entity_id: Optional[str] = None,
        entity_type: Optional[str] = None,
        # Source agent (#1396) — which fleet agent is capturing these memories
        source_agent: Optional[str] = None,
    ) -> RememberResult:
        """
        Extract claims from *conversation* and persist them to memory.

        Pipeline: extract → resolve entities → reconcile → (auto-stored in reconcile)

        Args:
            conversation: List of message dicts, raw text string, or any
                          object ExtractionPipeline.extract() accepts.
            session_id:   Associate memories with this session.
            scope:        Owner scope (default: 'user').

        Returns:
            RememberResult with counts and any soft errors encountered.
        """
        # Resolve effective owner — use request-supplied owner_id when provided
        effective_owner_id = owner_id or self.config.owner_id

        sid = session_id or self._session_id or _new_session_id()
        errors: List[str] = []
        warnings: List[str] = []

        if not hasattr(self, "_extraction_semaphore"):
            max_concurrent = max(1, _config_int(self.config, "max_concurrent_extractions", 4))
            self._extraction_semaphore = asyncio.Semaphore(max_concurrent)
        if not hasattr(self, "_zero_claim_outcomes"):
            self._zero_claim_outcomes = 0

        async def _bump_session_counters(memories_delta: int) -> None:
            """Best-effort session metric update (turn_count + memories_created)."""
            if self.storage is None:
                return
            try:
                if hasattr(self.storage, "increment_session_counters"):
                    await self.storage.increment_session_counters(
                        session_id=sid,
                        owner_id=effective_owner_id,
                        turn_delta=1,
                        memories_created_delta=memories_delta,
                    )
                    return
                existing = None
                if hasattr(self.storage, "get_session_by_sid"):
                    existing = await self.storage.get_session_by_sid(
                        session_id=sid,
                        owner_id=effective_owner_id,
                    )
                if existing is None:
                    return
                await self.storage.update_session(
                    sid,
                    owner_id=effective_owner_id,
                    turn_count=(existing.turn_count or 0) + 1,
                    memories_created=(existing.memories_created or 0) + max(0, memories_delta),
                )
            except Exception as exc:
                logger.warning(
                    "Failed to update session counters for session %s (owner=%s): %s",
                    sid, effective_owner_id, exc,
                )

        async def _finalize_result(result: RememberResult) -> RememberResult:
            await _bump_session_counters(result.claims_stored)
            return result

        # 0a. Validate + sanitize conversation input
        import cortex.api.plugin as _plugin_pkg
        conversation = _plugin_pkg.validate_conversation(conversation)
        conversation = _strip_transport_metadata_conversation(conversation)

        # 0b. Filter internal/system content before extraction (issue #866).
        # Engine-injected blocks (runtime context, memory injections, subagent
        # completion reports, heartbeats) produce garbage claims and are silently
        # dropped when extraction_block_internal=True (default).
        _block_internal = getattr(self.config, "extraction_block_internal", True)
        conversation, _internal_skipped = _filter_internal_content_conversation(
            conversation, block_internal=_block_internal
        )
        if _internal_skipped:
            logger.debug(
                "Skipped %d internal/system message(s) before extraction (session=%s)",
                _internal_skipped,
                sid,
            )

        # 0. Ensure session_record exists (FK constraint on semantic_claim.source_session)
        db_session_pk = None
        if self.storage is not None:
            try:
                session_rec = await self.storage.create_session(
                    session_id=sid,
                    owner_id=effective_owner_id,
                )
                db_session_pk = session_rec.id
            except Exception as exc:
                logger.debug("Session record creation (may already exist): %s", exc)
                try:
                    existing = await self.storage.get_session_by_sid(
                        session_id=sid,
                        owner_id=effective_owner_id,
                    )
                    if existing is not None:
                        db_session_pk = existing.id
                except Exception as lookup_exc:
                    logger.warning("Failed to look up existing session: %s", lookup_exc)

        # 0b-pre. Log episode event (evidence-first: always before extraction)
        # Delegates to cortex.capture.episode_adapter — best-effort, never blocks.
        _episode_event_id: Optional[str] = None
        if self.storage is not None:
            _episode_event_id = await _log_episode_event(
                storage=self.storage,
                session_id_pk=db_session_pk,
                conversation=conversation,
                owner_id=effective_owner_id,
                sid=sid,
            )

        # 0b. Check extraction_enabled toggle
        if not getattr(self.config, "extraction_enabled", True):
            logger.info(
                "Extraction disabled (extraction_enabled=False) — skipping for session %s",
                sid,
            )
            return await _finalize_result(RememberResult(
                session_id=sid,
                claims_extracted=0,
                claims_stored=0,
                entities_resolved=0,
                noise_filtered=0,
                errors=[],
                warnings=[],
                zero_claim_warning=False,
                ok=True,
            ))

        # Steps 1-4: Extraction → dedup → entity resolution → reconcile → embed
        # Delegated to cortex.capture.wake_pipeline.run_extraction()
        outcome = await _run_extraction(
            extractor=self.extractor,
            storage=self.storage,
            llm=self.llm,
            entity_resolver=self.entity_resolver,
            cornerstone_guardian=self.cornerstone_guardian,
            reconciler=self.reconciler,
            event_emitter=getattr(self, "event_emitter", None),
            config=self.config,
            extraction_semaphore=self._extraction_semaphore,
            conversation=conversation,
            sid=sid,
            db_session_pk=db_session_pk,
            effective_owner_id=effective_owner_id,
            episode_event_id=_episode_event_id,
            budget_check=self._budget_check,
            budget_consume=self._budget_consume,
            errors=errors,
            warnings=warnings,
            zero_claim_outcomes=self._zero_claim_outcomes,
            session_date=session_date,
            speaker_a=speaker_a,
            speaker_b=speaker_b,
            source_session_id=source_session_id,
            source_message_id=source_message_id,
            source_channel=source_channel,
            source_type=source_type,
            entity_id=entity_id,
            entity_type=entity_type,
            source_agent=source_agent,
        )
        # Write back mutable plugin state updated inside the pipeline
        self._zero_claim_outcomes = outcome.updated_zero_claim_outcomes

        # Update extraction metrics for quality gates (#565)
        if self._metrics is not None:
            try:
                await self._metrics.increment("extractions_run")
                if not hasattr(self, "_extraction_total"):
                    self._extraction_total = 0
                    self._extraction_success = 0
                self._extraction_total += 1
                if outcome.ok:
                    self._extraction_success += 1
                rate = self._extraction_success / self._extraction_total
                await self._metrics.set_gauge("extraction_success_rate", rate)
            except Exception as exc:
                logger.debug("Failed to update extraction metrics: %s", exc)

        # Queue failed extractions for retry during dream cycle
        if not outcome.ok and outcome.errors and self.storage is not None:
            try:
                from cortex.capture.extraction_queue import enqueue_failed_extraction
                error_summary = "; ".join(outcome.errors[:3])
                await enqueue_failed_extraction(
                    self.storage,
                    session_id=sid,
                    owner_id=effective_owner_id,
                    conversation=conversation,
                    error_message=error_summary,
                    source_session_id=source_session_id,
                    source_message_id=source_message_id,
                    source_channel=source_channel,
                    source_type=source_type,
                )
            except Exception as q_exc:
                logger.warning("Failed to queue extraction for retry: %s", q_exc)

        return await _finalize_result(RememberResult(
            session_id=sid,
            claims_extracted=outcome.claims_extracted,
            claims_stored=outcome.claims_stored,
            entities_resolved=outcome.entities_resolved,
            noise_filtered=outcome.noise_filtered,
            errors=outcome.errors,
            warnings=outcome.warnings,
            zero_claim_warning=outcome.zero_claim_warning,
            ok=outcome.ok,
        ))

    async def forget(
        self,
        memory_id: str,
        hard_delete: bool = False,
    ) -> ForgetResult:
        """
        Delete a memory by ID, with cornerstone protection.

        If the memory is a sealed cornerstone it will NOT be deleted —
        the caller must unseal_cornerstone() first.
        """
        # 1. Cornerstone protection check ------------------------------------
        if self.cornerstone_guardian is not None:
            try:
                is_cs = await self.cornerstone_guardian.is_cornerstone_subject(
                    subject=memory_id,
                    owner_id=self.config.owner_id,
                )
                if is_cs:
                    return ForgetResult(
                        memory_id=memory_id,
                        deleted=False,
                        reason="Memory is a sealed cornerstone — unseal first",
                    )
            except Exception as exc:
                err = f"Cornerstone check failed: {exc}"
                logger.warning(err)
                # Fail-safe: refuse deletion when protection check fails
                return ForgetResult(
                    memory_id=memory_id,
                    deleted=False,
                    reason=f"Cannot verify cornerstone status: {exc}",
                    errors=[err],
                )

        # 2. Storage delete ---------------------------------------------------
        if self.storage is None:
            return ForgetResult(
                memory_id=memory_id,
                deleted=False,
                reason="Storage not available",
                errors=["SQLiteAdapter not initialised"],
            )

        try:
            await self.storage.delete_claim(
                claim_id=memory_id,
                owner_id=self.config.owner_id,
                hard=hard_delete,
            )
            return ForgetResult(memory_id=memory_id, deleted=True, reason="ok")
        except Exception as exc:
            err = f"Delete failed: {exc}"
            logger.error(err, exc_info=True)
            return ForgetResult(
                memory_id=memory_id,
                deleted=False,
                reason=str(exc),
                errors=[err],
            )
