"""Experience database for PCB axial flux motor automated simulation.

Lightweight SQLite-based experience database for Phase 1:
- Auto-accumulate simulation results after each scan
- Similar case retrieval by topology + parameter proximity
- Feedback: recommend next-round parameter ranges based on results

All source is ASCII.
"""

from __future__ import annotations

import json
import math
import sqlite3
from datetime import datetime
from pathlib import Path
from typing import Any


# ---------------------------------------------------------------------------
# Database schema
# ---------------------------------------------------------------------------

SCHEMA = """
CREATE TABLE IF NOT EXISTS runs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    plan_id TEXT NOT NULL,
    topology TEXT NOT NULL DEFAULT 'SSSR',
    model_path TEXT,
    timestamp TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT 'OK',
    params_json TEXT NOT NULL,
    metrics_json TEXT NOT NULL,
    solve_time_s REAL,
    notes TEXT
);

CREATE INDEX IF NOT EXISTS idx_runs_topology ON runs(topology);
CREATE INDEX IF NOT EXISTS idx_runs_timestamp ON runs(timestamp);
CREATE INDEX IF NOT EXISTS idx_runs_plan_id ON runs(plan_id);
"""


# ---------------------------------------------------------------------------
# Experience database
# ---------------------------------------------------------------------------

class ExperienceDB:
    """SQLite-based experience database for simulation results."""

    def __init__(self, db_path: str | Path = "experience/experience.db"):
        self.db_path = Path(db_path)
        self.db_path.parent.mkdir(parents=True, exist_ok=True)
        self._conn = sqlite3.connect(str(self.db_path))
        self._conn.row_factory = sqlite3.Row
        self._conn.executescript(SCHEMA)
        self._conn.commit()

    def close(self) -> None:
        self._conn.close()

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()

    # -- Insertion --

    def add_run(
        self,
        plan_id: str,
        params: dict[str, float],
        metrics: dict[str, float],
        topology: str = "SSSR",
        model_path: str = "",
        status: str = "OK",
        solve_time_s: float = 0.0,
        notes: str = "",
    ) -> int:
        """Add a single simulation run result. Returns the run ID."""
        cursor = self._conn.execute(
            """INSERT INTO runs (plan_id, topology, model_path, timestamp, status,
               params_json, metrics_json, solve_time_s, notes)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            (
                plan_id,
                topology,
                model_path,
                datetime.now().isoformat(timespec="seconds"),
                status,
                json.dumps(params),
                json.dumps(metrics),
                solve_time_s,
                notes,
            ),
        )
        self._conn.commit()
        return cursor.lastrowid

    def add_runs_from_csv(self, csv_path: str | Path, plan_id: str,
                           topology: str = "SSSR", model_path: str = "") -> int:
        """Add all runs from a scan results CSV. Returns number added."""
        import csv as csv_mod
        count = 0
        with open(csv_path, "r", encoding="utf-8-sig", newline="") as f:
            reader = csv_mod.DictReader(f)
            for row in reader:
                if row.get("status") != "OK":
                    continue
                # Extract params (all columns that are not standard fields/metrics)
                standard = {"run_index", "status", "seconds", "error"}
                from .solver_core import METRIC_KEYS
                metric_set = set(METRIC_KEYS)
                params = {}
                metrics = {}
                for key, val in row.items():
                    if key in standard or not val:
                        continue
                    try:
                        fval = float(val)
                    except (ValueError, TypeError):
                        continue
                    if key in metric_set:
                        metrics[key] = fval
                    else:
                        params[key] = fval
                self.add_run(
                    plan_id=plan_id, params=params, metrics=metrics,
                    topology=topology, model_path=model_path,
                    status=row.get("status", "OK"),
                    solve_time_s=float(row.get("seconds", 0) or 0),
                )
                count += 1
        return count

    # -- Query --

    def get_run(self, run_id: int) -> dict[str, Any] | None:
        """Get a single run by ID."""
        row = self._conn.execute("SELECT * FROM runs WHERE id = ?", (run_id,)).fetchone()
        return self._row_to_dict(row) if row else None

    def get_all_runs(self, topology: str | None = None,
                      limit: int = 100) -> list[dict[str, Any]]:
        """Get all runs, optionally filtered by topology."""
        if topology:
            rows = self._conn.execute(
                "SELECT * FROM runs WHERE topology = ? ORDER BY timestamp DESC LIMIT ?",
                (topology, limit),
            ).fetchall()
        else:
            rows = self._conn.execute(
                "SELECT * FROM runs ORDER BY timestamp DESC LIMIT ?",
                (limit,),
            ).fetchall()
        return [self._row_to_dict(r) for r in rows]

    def find_similar(
        self,
        params: dict[str, float],
        topology: str = "SSSR",
        top_n: int = 5,
        tolerance: float = 0.5,
    ) -> list[dict[str, Any]]:
        """Find similar runs based on parameter proximity.

        Args:
            params: target parameter values to match against.
            topology: filter by topology.
            top_n: number of results to return.
            tolerance: relative tolerance for parameter matching (0.5 = 50%).

        Returns:
            List of similar runs, sorted by similarity score (closest first).
        """
        candidates = self.get_all_runs(topology=topology, limit=500)
        scored = []
        for run in candidates:
            run_params = run.get("params", {})
            # Compute average relative distance across shared parameters
            distances = []
            shared = 0
            for key, target_val in params.items():
                if key in run_params and target_val != 0:
                    rel_dist = abs(run_params[key] - target_val) / abs(target_val)
                    distances.append(rel_dist)
                    shared += 1
            if shared == 0:
                continue
            avg_dist = sum(distances) / len(distances)
            if avg_dist <= tolerance:
                scored.append((avg_dist, run))
        scored.sort(key=lambda x: x[0])
        return [run for _, run in scored[:top_n]]

    def get_stats(self, topology: str | None = None) -> dict[str, Any]:
        """Get database statistics."""
        if topology:
            total = self._conn.execute(
                "SELECT COUNT(*) FROM runs WHERE topology = ?", (topology,)
            ).fetchone()[0]
        else:
            total = self._conn.execute("SELECT COUNT(*) FROM runs").fetchone()[0]
        plans = self._conn.execute(
            "SELECT COUNT(DISTINCT plan_id) FROM runs"
        ).fetchone()[0]
        return {"total_runs": total, "distinct_plans": plans, "db_path": str(self.db_path)}

    # -- Feedback / Recommendation --

    def recommend_next_round(
        self,
        params: dict[str, float],
        metrics: dict[str, float],
        target_metric: str = "efficiency_pct",
        direction: str = "maximize",
        topology: str = "SSSR",
    ) -> dict[str, Any]:
        """Recommend next-round parameter ranges based on current results.

        Uses simple trend analysis: find similar runs in the experience DB,
        compare their metric values, and recommend parameter directions.

        Args:
            params: current parameter values.
            metrics: current metric values.
            target_metric: metric to optimize.
            direction: 'maximize' or 'minimize'.
            topology: motor topology.

        Returns:
            dict with recommendations for each parameter.
        """
        similar = self.find_similar(params, topology=topology, top_n=10, tolerance=1.0)
        if not similar:
            return {
                "status": "insufficient_data",
                "message": "Not enough similar runs in experience DB for recommendation. "
                           "Accumulate more simulation results first.",
                "recommendations": {},
            }

        current_val = metrics.get(target_metric)
        if current_val is None:
            return {
                "status": "missing_metric",
                "message": f"Target metric '{target_metric}' not found in current results.",
                "recommendations": {},
            }

        # Analyze each parameter: find runs where this parameter differs
        recommendations = {}
        for param_name, current_param_val in params.items():
            # Collect (param_value, metric_value) pairs from similar runs
            pairs = []
            for run in similar:
                if param_name in run["params"] and target_metric in run["metrics"]:
                    pairs.append((
                        run["params"][param_name],
                        run["metrics"][target_metric],
                    ))
            if len(pairs) < 2:
                recommendations[param_name] = {
                    "status": "insufficient_data",
                    "message": f"Not enough data points for parameter '{param_name}'.",
                }
                continue

            # Simple linear trend: compute correlation direction
            pairs.sort(key=lambda x: x[0])
            n = len(pairs)
            mean_x = sum(p[0] for p in pairs) / n
            mean_y = sum(p[1] for p in pairs) / n
            cov = sum((p[0] - mean_x) * (p[1] - mean_y) for p in pairs) / n
            var_x = sum((p[0] - mean_x) ** 2 for p in pairs) / n
            if var_x == 0:
                recommendations[param_name] = {
                    "status": "no_variation",
                    "message": f"Parameter '{param_name}' has no variation in similar runs.",
                }
                continue
            slope = cov / var_x  # metric change per unit parameter change

            # Recommend direction
            if direction == "maximize":
                recommend_increase = slope > 0
            else:
                recommend_increase = slope < 0

            # Suggest new range: current value +/- 20%, biased toward recommendation
            delta = abs(current_param_val) * 0.2 if current_param_val != 0 else 0.5
            if recommend_increase:
                new_range = [current_param_val, current_param_val + delta * 2]
            else:
                new_range = [max(0, current_param_val - delta * 2), current_param_val]

            recommendations[param_name] = {
                "status": "ok",
                "current_value": current_param_val,
                "trend_slope": round(slope, 6),
                "recommend_increase": recommend_increase,
                "suggested_range": [round(new_range[0], 4), round(new_range[1], 4)],
                "data_points": n,
            }

        return {
            "status": "ok",
            "target_metric": target_metric,
            "direction": direction,
            "current_value": current_val,
            "similar_runs_used": len(similar),
            "recommendations": recommendations,
        }

    # -- Internal --

    def _row_to_dict(self, row: sqlite3.Row) -> dict[str, Any]:
        d = dict(row)
        d["params"] = json.loads(d.get("params_json", "{}"))
        d["metrics"] = json.loads(d.get("metrics_json", "{}"))
        del d["params_json"]
        del d["metrics_json"]
        return d
