"""Contextual memory for MixLift — store analysis summaries across runs.

Stores per-analysis summaries locally so subsequent runs on the same dataset
can show deltas ("Meta ROAS improved from 2.3x to 2.8x since last analysis").

Memory is keyed by a fingerprint of sorted channel names + CSV filename stem,
so adding rows (weekly appends) matches the same dataset as long as channels
are unchanged.
"""

import hashlib
import json
import logging
from datetime import datetime
from pathlib import Path

logger = logging.getLogger(__name__)

MEMORY_DIR = Path.home() / ".mixlift" / "memory"
MAX_ENTRIES_PER_DATASET = 10


def compute_fingerprint(channel_names: list[str], csv_path: str) -> str:
    """Compute a stable fingerprint for a dataset based on its channels and filename."""
    stem = Path(csv_path).stem
    sorted_channels = "|".join(sorted(channel_names))
    raw = f"{sorted_channels}|{stem}"
    return hashlib.sha256(raw.encode()).hexdigest()[:16]


def _memory_file(fingerprint: str) -> Path:
    return MEMORY_DIR / f"{fingerprint}.json"


def load_previous(fingerprint: str) -> dict | None:
    """Load the most recent analysis for this dataset fingerprint."""
    path = _memory_file(fingerprint)
    try:
        if path.exists():
            data = json.loads(path.read_text())
            analyses = data.get("analyses", [])
            if analyses:
                return analyses[-1]
    except Exception as e:
        logger.warning(f"Could not load memory: {e}")
    return None


def save_analysis(fingerprint: str, csv_path: str, summary: dict) -> None:
    """Save an analysis summary for later comparison."""
    path = _memory_file(fingerprint)

    data = {"fingerprint": fingerprint, "dataset_label": Path(csv_path).name, "analyses": []}
    try:
        MEMORY_DIR.mkdir(parents=True, exist_ok=True)
        if path.exists():
            data = json.loads(path.read_text())
    except Exception:
        pass

    data["analyses"].append(summary)

    # FIFO eviction
    if len(data["analyses"]) > MAX_ENTRIES_PER_DATASET:
        data["analyses"] = data["analyses"][-MAX_ENTRIES_PER_DATASET:]

    try:
        path.write_text(json.dumps(data, indent=2, default=str))
    except Exception as e:
        logger.warning(f"Could not save memory: {e}")


def build_summary(
    roas_estimates: dict[str, float],
    roas_ci: dict[str, tuple[float, float]],
    saturation_lights: dict,
    optimization: dict,
    channel_contributions: dict[str, float],
    model_fit: dict,
    data_weeks: float,
    analysis_badge: dict | None,
) -> dict:
    """Build a compact summary dict suitable for storage and comparison."""
    # Contribution shares (% of total)
    total_contrib = sum(channel_contributions.values()) or 1.0
    contrib_shares = {
        ch: round(v / total_contrib * 100, 1)
        for ch, v in channel_contributions.items()
    }

    # Allocation shares
    current_alloc = optimization.get("current_allocation", {})
    total_budget = sum(current_alloc.values()) if isinstance(current_alloc, dict) else 0
    alloc_shares = {}
    if isinstance(current_alloc, dict) and total_budget > 0:
        alloc_shares = {
            ch: round(v / total_budget * 100, 1)
            for ch, v in current_alloc.items()
        }

    # Saturation percentages
    sat_pcts = {}
    for ch, light in saturation_lights.items():
        sat_pcts[ch] = light.get("saturation_pct", 0)

    return {
        "timestamp": datetime.now().isoformat(),
        "channels": sorted(roas_estimates.keys()),
        "data_weeks": round(data_weeks, 1),
        "roas": {
            ch: {
                "mean": round(v, 3),
                "ci_low": round(roas_ci[ch][0], 3),
                "ci_high": round(roas_ci[ch][1], 3),
            }
            for ch, v in roas_estimates.items()
        },
        "contribution_share": contrib_shares,
        "allocation_share": alloc_shares,
        "saturation_pct": sat_pcts,
        "optimization_lift_pct": optimization.get("expected_lift_pct", 0),
        "r_squared": model_fit.get("r_squared"),
        "badge": analysis_badge["type"] if analysis_badge else None,
    }


def compute_deltas(current: dict, previous: dict) -> dict:
    """Compare current analysis to a previous one and produce delta narratives.

    Uses CI overlap heuristic from DS Manager methodology:
    - 90% CIs don't overlap previous mean → "clearly changed"
    - Otherwise check magnitude (>5% relative change to suppress noise)
    """
    deltas = {}
    prev_ts = previous.get("timestamp", "unknown")

    for channel in current.get("roas", {}):
        cur = current["roas"].get(channel, {})
        prev = previous.get("roas", {}).get(channel)
        if not prev:
            deltas[channel] = {
                "status": "new",
                "narrative": f"{channel}: New channel (not in previous analysis).",
            }
            continue

        cur_mean = cur.get("mean", 0)
        prev_mean = prev.get("mean", 0)
        cur_ci_low = cur.get("ci_low", 0)
        cur_ci_high = cur.get("ci_high", 0)

        if prev_mean == 0:
            continue

        change_pct = ((cur_mean - prev_mean) / prev_mean) * 100
        direction = "increased" if change_pct > 0 else "decreased"
        abs_change = abs(change_pct)

        # Suppress noise: <5% relative change
        if abs_change < 5:
            deltas[channel] = {
                "status": "stable",
                "roas_previous": round(prev_mean, 2),
                "roas_current": round(cur_mean, 2),
                "change_pct": round(change_pct, 1),
                "confidence": "low",
                "narrative": (
                    f"{channel} ROAS is essentially unchanged "
                    f"({prev_mean:.2f}x → {cur_mean:.2f}x). "
                    f"The difference is within normal posterior uncertainty."
                ),
            }
            continue

        # CI overlap check: does the 90% CI exclude the previous mean?
        if prev_mean < cur_ci_low or prev_mean > cur_ci_high:
            confidence = "high"
            qualifier = "clearly"
        else:
            confidence = "moderate"
            qualifier = "likely"

        deltas[channel] = {
            "status": "changed",
            "roas_previous": round(prev_mean, 2),
            "roas_current": round(cur_mean, 2),
            "change_pct": round(change_pct, 1),
            "confidence": confidence,
            "narrative": (
                f"{channel} ROAS {qualifier} {direction} from "
                f"{prev_mean:.2f}x to {cur_mean:.2f}x ({change_pct:+.0f}%)."
            ),
        }

    # Check for dropped channels
    for channel in previous.get("roas", {}):
        if channel not in current.get("roas", {}):
            deltas[channel] = {
                "status": "dropped",
                "narrative": f"{channel}: No longer in dataset (was in previous analysis).",
            }

    return {
        "previous_analysis": prev_ts,
        "changes": deltas,
    }
