"""
cortex/core/feedback.py — Memory Feedback System (Module 12 / M12).

Tracks which retrieved memories were helpful or harmful, queues feedback
for dream-cycle processing, and computes per-memory helpfulness scores
that influence future retrieval ranking.

This is a closed-loop learning signal: retrieval serves memories →
user/agent rates them → feedback adjusts future retrieval rankings.

Lifecycle:
    1. RetrievalPipeline serves a memory item
    2. Caller calls submit_feedback(memory_id, helpful) → status='pending'
    3. Dream cycle calls process_pending() → status='applied'
    4. RetrievalPipeline calls get_score(memory_id) → influences ranking

Design decisions:
    - Scores are simple counting ratios (helpful/total), not ML models
    - Neutral prior (0.5) when no feedback exists — no bias
    - Only 'applied' feedback counts in scores (prevents premature influence)
    - Individual apply failures don't abort the batch (resilient processing)

Dependencies: M1 (StorageAdapter) — create_feedback, get_pending_feedback,
              apply_feedback, get_feedback_by_target.
Used by: CortexPlugin (M13) for the feedback() API method,
         Dream cycle orchestrator for process_pending().
"""

from __future__ import annotations

import logging
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import List, Optional, TYPE_CHECKING

if TYPE_CHECKING:
    from cortex.storage.adapter import StorageAdapter

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------

@dataclass
class MemoryFeedback:
    """A single feedback record mirroring the memory_feedback table (schema v3)."""
    id: str
    owner_id: str
    target_type: str          # 'claim' | 'cornerstone' | 'procedure' | 'entity' | 'edge'
    target_id: str            # the memory_id being rated
    feedback_type: str        # 'helpful' | 'harmful' | 'wrong' | 'sensitive' | …
    feedback_value: Optional[str]   # optional extra payload
    source: str               # 'operator' | 'agent' | 'user' | 'system'
    status: str               # 'pending' | 'applied' | 'dismissed'
    context: Optional[str]    # free-form context from the caller
    created_at: str
    applied_at: Optional[str]


@dataclass
class FeedbackScore:
    """Aggregated helpfulness ratio for a single memory."""
    memory_id: str
    total: int
    helpful: int
    harmful: int
    score: float              # helpful / total  (0.0–1.0); 0.5 if no data
    pending: int


# ---------------------------------------------------------------------------
# FeedbackSystem
# ---------------------------------------------------------------------------

class FeedbackSystem:
    """
    Accept, queue, and process memory-quality feedback.

    Typical lifecycle
    -----------------
    1. Retrieval pipeline serves a memory item.
    2. Caller invokes ``submit_feedback(memory_id, helpful, context)``
       which writes a 'pending' record to the feedback queue.
    3. During the dream cycle the orchestrator calls ``process_pending()``
       which applies all pending records and updates their status.
    4. ``get_score(memory_id)`` returns the running helpfulness ratio which
       retrieval scoring uses as an access-count-style bonus.

    Storage contract
    ----------------
    The class delegates all I/O to ``StorageAdapter``, which must expose:
    - ``create_feedback(target_type, target_id, feedback_type, **kwargs) -> MemoryFeedback``
    - ``get_pending_feedback(owner_id) -> List[MemoryFeedback]``
    - ``apply_feedback(feedback_id) -> MemoryFeedback``
    - ``get_feedback_by_target(target_id, owner_id) -> List[MemoryFeedback]``
      (falls back to get_pending_feedback + filtering if not available)

    All public methods are ``async``.
    """

    def __init__(self, storage: "StorageAdapter", owner_id: str = "default") -> None:
        self.storage = storage
        self.owner_id = owner_id

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    async def submit_feedback(
        self,
        memory_id: str,
        helpful: bool,
        context: Optional[str] = None,
        *,
        target_type: str = "claim",
        source: str = "operator",
        owner_id: Optional[str] = None,
    ) -> MemoryFeedback:
        """
        Record positive or negative feedback for a retrieved memory.

        Parameters
        ----------
        memory_id:
            The ID of the memory (claim, cornerstone, etc.) being rated.
        helpful:
            ``True`` → 'helpful' feedback; ``False`` → 'harmful' feedback.
        context:
            Optional free-text note from the caller (e.g. the query that
            produced this retrieval, or a human annotation).
        target_type:
            The kind of memory being rated (default: 'claim').
        source:
            Who is submitting (default: 'operator').
        owner_id:
            Override the instance-level owner (default: self.owner_id).

        Returns
        -------
        MemoryFeedback
            The newly created, status='pending' feedback record.
        """
        feedback_type = "helpful" if helpful else "harmful"
        oid = owner_id or self.owner_id

        record = await self.storage.create_feedback(
            target_type=target_type,
            target_id=memory_id,
            feedback_type=feedback_type,
            feedback_value=None,
            source=source,
            owner_id=oid,
            context=context,
            status="pending",
        )
        return record

    async def get_pending(
        self,
        owner_id: Optional[str] = None,
    ) -> List[MemoryFeedback]:
        """
        Return all feedback records that are still status='pending'.

        These are consumed by ``process_pending()`` during dream cycles.
        Human operators can inspect the queue before processing runs.

        Parameters
        ----------
        owner_id:
            Scope to a specific owner (default: self.owner_id).

        Returns
        -------
        List[MemoryFeedback]
            Pending feedback records, oldest first.
        """
        oid = owner_id or self.owner_id
        return await self.storage.get_pending_feedback(oid)

    async def process_pending(
        self,
        owner_id: Optional[str] = None,
    ) -> int:
        """
        Process (apply) all pending feedback records.

        Called by the dream-cycle orchestrator. For each pending record
        the method delegates to ``storage.apply_feedback`` which marks the
        record as 'applied' and sets ``applied_at``.

        No ML: helpfulness scores are pure counting ratios. Retrieval
        scoring picks up the updated ratios via ``get_score()``.

        Parameters
        ----------
        owner_id:
            Scope to a specific owner (default: self.owner_id).

        Returns
        -------
        int
            Number of feedback records successfully applied.
        """
        oid = owner_id or self.owner_id
        pending = await self.storage.get_pending_feedback(oid)
        applied = 0
        for record in pending:
            try:
                await self.storage.apply_feedback(record.id)
                applied += 1
                # Changelog: record that feedback was applied to the target claim
                try:
                    history = await self.storage.get_changelog(record.target_id)
                    next_ver = len(history) + 1
                    await self.storage.log_changelog(
                        claim_id=record.target_id,
                        version=next_ver,
                        content_snapshot=None,
                        action="feedback_applied",
                        reason=f"{record.feedback_type} feedback applied (source: {record.source})",
                        changed_by="feedback",
                        diff_summary=None,
                    )
                except Exception:
                    pass  # changelog failure must not abort the feedback batch
            except Exception as exc:
                # Individual failures must not abort the entire batch.
                # Unprocessed records remain 'pending' for the next cycle.
                logger.warning(
                    "Failed to apply feedback record %s: %s",
                    getattr(record, "id", "<unknown>"),
                    exc,
                )
        return applied

    async def get_scores(
        self,
        memory_ids: List[str],
        owner_id: Optional[str] = None,
    ) -> dict[str, "FeedbackScore"]:
        """Batch-fetch helpfulness scores for multiple memories.

        Returns a dict mapping each memory_id to its FeedbackScore.
        This avoids the N+1 query pattern of calling get_score() in a loop.

        Parameters
        ----------
        memory_ids:
            List of memory IDs to score.
        owner_id:
            Scope to a specific owner (default: self.owner_id).

        Returns
        -------
        dict[str, FeedbackScore]
            Mapping of memory_id → FeedbackScore.
        """
        results: dict[str, FeedbackScore] = {}
        for mid in memory_ids:
            results[mid] = await self.get_score(mid, owner_id=owner_id)
        return results

    async def get_score(
        self,
        memory_id: str,
        owner_id: Optional[str] = None,
    ) -> FeedbackScore:
        """
        Compute the aggregated helpfulness ratio for a single memory.

        Score = helpful_count / total_count, or 0.5 when no feedback exists
        (neutral prior — neither boost nor penalty).

        Only 'applied' feedback is included in the score so that fresh
        submissions don't influence retrieval until the dream cycle runs.

        Parameters
        ----------
        memory_id:
            The ID of the memory whose score is requested.
        owner_id:
            Scope to a specific owner (default: self.owner_id).

        Returns
        -------
        FeedbackScore
            Breakdown of counts and the aggregated ratio [0.0–1.0].
        """
        oid = owner_id or self.owner_id
        all_records = await self._get_all_feedback_for_target(memory_id, oid)

        applied = [r for r in all_records if r.status == "applied"]
        pending = [r for r in all_records if r.status == "pending"]

        helpful_count = sum(1 for r in applied if r.feedback_type == "helpful")
        harmful_count = sum(1 for r in applied if r.feedback_type == "harmful")
        total = helpful_count + harmful_count

        score = (helpful_count / total) if total > 0 else 0.5

        return FeedbackScore(
            memory_id=memory_id,
            total=total,
            helpful=helpful_count,
            harmful=harmful_count,
            score=round(score, 4),
            pending=len(pending),
        )

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    async def _get_all_feedback_for_target(
        self,
        target_id: str,
        owner_id: str,
    ) -> List[MemoryFeedback]:
        """
        Retrieve all feedback records (any status) for a given target.

        Tries the optional ``get_feedback_by_target`` adapter method first;
        falls back to fetching all applied + pending and filtering in-memory
        so the system works even with minimal StorageAdapter implementations.
        """
        # Prefer a dedicated adapter method when available
        if hasattr(self.storage, "get_feedback_by_target"):
            return await self.storage.get_feedback_by_target(target_id, owner_id)

        # Fallback: combine pending + applied via available methods
        records: List[MemoryFeedback] = []

        # Pending (always available per StorageAdapter ABC)
        pending = await self.storage.get_pending_feedback(owner_id)
        records.extend(r for r in pending if r.target_id == target_id)

        # Applied — try the optional method
        if hasattr(self.storage, "get_applied_feedback"):
            applied = await self.storage.get_applied_feedback(owner_id)
            records.extend(r for r in applied if r.target_id == target_id)

        return records
