"""
Custom outlier detection agent — example of an LLM-powered SciLink custom agent.

Demonstrates:
- Using ``self.model.generate_content()`` to call the LLM.
- Using ``self._parse_llm_response()`` to extract JSON from the LLM reply.
- Combining algorithmic analysis with LLM-generated scientific claims.

Usage
-----
  scilink analyze --agents examples/custom_outlier_agent.py

Or upload via the "Tools & Agents" tab in the UI.
"""

import json
import logging
import numpy as np
from pathlib import Path
from typing import Any, Dict, Optional

from scilink.agents.exp_agents.base_agent import BaseAnalysisAgent


class OutlierDetectionAgent(BaseAnalysisAgent):
    """Detects statistical outliers in 1-D data and uses the LLM to interpret them."""

    AGENT_NAME = "OutlierDetectionAgent"
    AGENT_DESCRIPTION = (
        "Statistical outlier detection for 1-D data (spectra, curves, time "
        "series). Combines IQR/z-score analysis with LLM interpretation. "
        "Accepts CSV or NPY files."
    )
    AGENT_SHORT_NAME = "Outlier"

    Z_SCORE_THRESHOLD = 3.0
    IQR_MULTIPLIER = 1.5

    def analyze(self, data, system_info=None, objective=None, hints=None, **kwargs):
        data_path, data_paths, data_array, error = self._parse_data_input(data)
        if error:
            return {"status": "error", "error": {"error": error},
                    "detailed_analysis": "", "scientific_claims": [],
                    "output_directory": str(self.output_dir)}

        system_info = self._handle_system_info(system_info)

        try:
            x, y, x_label, y_label = self._load_xy(
                data_path or (data_paths[0] if data_paths else None),
                data_array, system_info or {},
            )
        except Exception as exc:
            return {"status": "error", "error": {"error": str(exc)},
                    "detailed_analysis": "", "scientific_claims": [],
                    "output_directory": str(self.output_dir)}

        report = self._detect_outliers(x, y, x_label, y_label, data_path)
        llm_result = self._interpret_with_llm(
            report["summary"], system_info, objective, hints,
        )

        if llm_result:
            detailed = llm_result.get("detailed_analysis", report["summary"])
            claims = llm_result.get("scientific_claims", [])
        else:
            detailed = report["summary"]
            claims = []

        (self.output_dir / "outlier_report.txt").write_text(detailed)

        return {
            "status": "success",
            "detailed_analysis": detailed,
            "scientific_claims": claims,
            "output_directory": str(self.output_dir),
            "outlier_count": report["n_outliers"],
        }

    def _load_xy(self, path, array, system_info):
        """Load x/y arrays from a file path or pre-loaded array."""
        x_label = system_info.get("x_axis", "x")
        y_label = system_info.get("y_axis", "Intensity")

        if array is not None:
            arr = np.asarray(array, dtype=float)
            if arr.ndim == 1:
                return np.arange(len(arr)), arr, x_label, y_label
            return arr[:, 0], arr[:, 1], x_label, y_label

        path = Path(path)
        if path.suffix.lower() in {".csv", ".txt", ".tsv"}:
            import csv
            rows, header = [], None
            with open(path, newline="") as fh:
                for row in csv.reader(fh):
                    try:
                        rows.append([float(v) for v in row])
                    except ValueError:
                        if header is None:
                            header = row
            arr = np.array(rows)
            if header and len(header) >= 2:
                x_label, y_label = header[0].strip(), header[1].strip()
            return arr[:, 0], arr[:, 1], x_label, y_label

        if path.suffix.lower() == ".npy":
            arr = np.load(str(path))
            if arr.ndim == 1:
                return np.arange(len(arr)), arr, x_label, y_label
            return arr[:, 0], arr[:, 1], x_label, y_label

        raise ValueError(f"Unsupported file type: {path.suffix}")

    def _detect_outliers(self, x, y, x_label, y_label, data_path):
        """Run IQR + z-score detection and return a report dict with summary string."""
        mean, std = float(np.mean(y)), float(np.std(y))
        q1, q3 = float(np.percentile(y, 25)), float(np.percentile(y, 75))
        iqr = q3 - q1

        z_scores = np.abs((y - mean) / std) if std > 0 else np.zeros_like(y)
        iqr_lo = q1 - self.IQR_MULTIPLIER * iqr
        iqr_hi = q3 + self.IQR_MULTIPLIER * iqr
        mask = (z_scores > self.Z_SCORE_THRESHOLD) | (y < iqr_lo) | (y > iqr_hi)
        indices = np.where(mask)[0]

        lines = [
            f"Outlier Detection — {data_path or 'array input'}",
            f"Points: {len(y)}, Outliers: {len(indices)} ({len(indices)/max(len(y),1)*100:.2f}%)",
            f"Stats: mean={mean:.4f}, std={std:.4f}, IQR={iqr:.4f}",
            f"Bounds: IQR [{iqr_lo:.4f}, {iqr_hi:.4f}], z-threshold={self.Z_SCORE_THRESHOLD}",
        ]
        for i in indices[:15]:
            lines.append(f"  {x_label}={x[i]:.4f}  {y_label}={y[i]:.4f}  z={z_scores[i]:.2f}")
        if len(indices) > 15:
            lines.append(f"  ... and {len(indices) - 15} more")

        return {"n_outliers": int(len(indices)), "summary": "\n".join(lines)}

    def _interpret_with_llm(self, summary, system_info, objective, hints):
        """Send findings to the LLM and return parsed JSON, or None on failure."""
        prompt_parts = [
            "You are a scientific data analyst. Given outlier detection results, "
            "return a JSON object with:\n"
            '- "detailed_analysis": multi-paragraph interpretation string\n'
            '- "scientific_claims": list of objects with keys "claim", '
            '"scientific_impact", "has_anyone_question", "keywords"\n',
            f"\n## Results\n{summary}",
        ]
        if system_info:
            prompt_parts.append(f"\n## Metadata\n{json.dumps(system_info, indent=2)}")
        if objective:
            prompt_parts.append(f"\n## Objective\n{objective}")
        if hints:
            prompt_parts.append(f"\n## Hints\n{hints}")

        try:
            response = self.model.generate_content(
                contents=prompt_parts,
                generation_config=self.generation_config,
                safety_settings=self.safety_settings,
            )
            result_json, error = self._parse_llm_response(response)
            if error:
                logging.warning(f"LLM parsing failed: {error}")
                return None
            return result_json
        except Exception as exc:
            logging.warning(f"LLM call failed: {exc}")
            return None
