"""Goal persistence and file-based loading for the autonomous agent loop."""

from __future__ import annotations

import json
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from database import Database
from logging_config import get_logger

log = get_logger(__name__)


@dataclass
class GoalEntry:
    """A persistent goal that drives proactive agent behaviour."""

    id: str
    name: str
    description: str
    enabled: bool
    check_interval: int  # seconds between runs
    sensor_prompt: str  # what to ask the LLM to observe
    action_prompt: str  # what to do when triggered
    autonomy_level: str  # auto | notify | approve
    skills_allowed: list[str]
    last_run_at: str | None
    created_at: str
    updated_at: str
    metadata: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "name": self.name,
            "description": self.description,
            "enabled": self.enabled,
            "check_interval": self.check_interval,
            "sensor_prompt": self.sensor_prompt,
            "action_prompt": self.action_prompt,
            "autonomy_level": self.autonomy_level,
            "skills_allowed": self.skills_allowed,
            "last_run_at": self.last_run_at,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
            "metadata": self.metadata,
        }

    def to_summary_dict(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "name": self.name,
            "description": self.description,
            "enabled": self.enabled,
            "check_interval": self.check_interval,
            "autonomy_level": self.autonomy_level,
            "last_run_at": self.last_run_at,
        }


@dataclass
class GoalRunEntry:
    """A single execution record of a goal."""

    id: str
    goal_id: str
    started_at: str
    finished_at: str | None
    status: str  # running | acted | skipped | error | pending_approval
    observation: str | None
    decision: str | None
    result: str | None
    session_id: str | None
    metadata: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "goal_id": self.goal_id,
            "started_at": self.started_at,
            "finished_at": self.finished_at,
            "status": self.status,
            "observation": self.observation,
            "decision": self.decision,
            "result": self.result,
            "session_id": self.session_id,
            "metadata": self.metadata,
        }


class GoalStore:
    """SQLite-backed goal store."""

    def __init__(self, db: Database) -> None:
        self._db = db
        self._log = log.bind(component="goal_store")

    @staticmethod
    def _row_to_entry(row: Any) -> GoalEntry:
        return GoalEntry(
            id=row["id"],
            name=row["name"],
            description=row["description"],
            enabled=bool(row["enabled"]),
            check_interval=row["check_interval"],
            sensor_prompt=row["sensor_prompt"],
            action_prompt=row["action_prompt"],
            autonomy_level=row["autonomy_level"],
            skills_allowed=json.loads(row["skills_allowed"]),
            last_run_at=row["last_run_at"],
            created_at=row["created_at"],
            updated_at=row["updated_at"],
            metadata=json.loads(row["metadata"]),
        )

    @staticmethod
    def _row_to_run(row: Any) -> GoalRunEntry:
        return GoalRunEntry(
            id=row["id"],
            goal_id=row["goal_id"],
            started_at=row["started_at"],
            finished_at=row["finished_at"],
            status=row["status"],
            observation=row["observation"],
            decision=row["decision"],
            result=row["result"],
            session_id=row["session_id"],
            metadata=json.loads(row["metadata"]),
        )

    # -- Goal CRUD --

    def add(
        self,
        name: str,
        description: str,
        sensor_prompt: str,
        action_prompt: str = "",
        check_interval: int = 300,
        autonomy_level: str = "auto",
        skills_allowed: list[str] | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> GoalEntry:
        now = datetime.now(timezone.utc).isoformat()
        entry = GoalEntry(
            id=uuid.uuid4().hex[:12],
            name=name.strip() or "Untitled Goal",
            description=description.strip(),
            enabled=True,
            check_interval=check_interval,
            sensor_prompt=sensor_prompt,
            action_prompt=action_prompt,
            autonomy_level=autonomy_level,
            skills_allowed=skills_allowed or [],
            last_run_at=None,
            created_at=now,
            updated_at=now,
            metadata=metadata or {},
        )

        self._db.conn.execute(
            """INSERT INTO goals
               (id, name, description, enabled, check_interval, sensor_prompt,
                action_prompt, autonomy_level, skills_allowed, last_run_at,
                created_at, updated_at, metadata)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            (
                entry.id,
                entry.name,
                entry.description,
                int(entry.enabled),
                entry.check_interval,
                entry.sensor_prompt,
                entry.action_prompt,
                entry.autonomy_level,
                json.dumps(entry.skills_allowed),
                entry.last_run_at,
                entry.created_at,
                entry.updated_at,
                json.dumps(entry.metadata, ensure_ascii=False),
            ),
        )
        self._db.conn.commit()
        self._log.info("goal_added", goal_id=entry.id, name=entry.name)
        return entry

    def get(self, goal_id: str) -> GoalEntry:
        row = self._db.conn.execute(
            "SELECT * FROM goals WHERE id = ?", (goal_id,)
        ).fetchone()
        if row is None:
            raise FileNotFoundError(f"Goal not found: {goal_id}")
        return self._row_to_entry(row)

    def list_all(self, limit: int = 200) -> list[GoalEntry]:
        rows = self._db.conn.execute(
            "SELECT * FROM goals ORDER BY created_at DESC LIMIT ?", (limit,)
        ).fetchall()
        return [self._row_to_entry(r) for r in rows]

    def list_enabled(self) -> list[GoalEntry]:
        rows = self._db.conn.execute(
            "SELECT * FROM goals WHERE enabled = 1 ORDER BY created_at DESC"
        ).fetchall()
        return [self._row_to_entry(r) for r in rows]

    def get_due(self) -> list[GoalEntry]:
        """Return enabled goals whose check_interval has elapsed since last run."""
        now = datetime.now(timezone.utc)
        due: list[GoalEntry] = []
        for goal in self.list_enabled():
            if goal.last_run_at is None:
                due.append(goal)
                continue
            last = datetime.fromisoformat(goal.last_run_at)
            if (now - last).total_seconds() >= goal.check_interval:
                due.append(goal)
        return due

    def update(
        self,
        goal_id: str,
        *,
        name: str | None = None,
        description: str | None = None,
        enabled: bool | None = None,
        check_interval: int | None = None,
        sensor_prompt: str | None = None,
        action_prompt: str | None = None,
        autonomy_level: str | None = None,
        skills_allowed: list[str] | None = None,
        last_run_at: str | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> GoalEntry:
        entry = self.get(goal_id)
        now = datetime.now(timezone.utc).isoformat()

        if name is not None:
            entry.name = name.strip() or entry.name
        if description is not None:
            entry.description = description.strip()
        if enabled is not None:
            entry.enabled = enabled
        if check_interval is not None:
            entry.check_interval = check_interval
        if sensor_prompt is not None:
            entry.sensor_prompt = sensor_prompt
        if action_prompt is not None:
            entry.action_prompt = action_prompt
        if autonomy_level is not None:
            entry.autonomy_level = autonomy_level
        if skills_allowed is not None:
            entry.skills_allowed = skills_allowed
        if last_run_at is not None:
            entry.last_run_at = last_run_at
        if metadata is not None:
            entry.metadata = metadata
        entry.updated_at = now

        self._db.conn.execute(
            """UPDATE goals
               SET name = ?, description = ?, enabled = ?, check_interval = ?,
                   sensor_prompt = ?, action_prompt = ?, autonomy_level = ?,
                   skills_allowed = ?, last_run_at = ?, updated_at = ?, metadata = ?
               WHERE id = ?""",
            (
                entry.name,
                entry.description,
                int(entry.enabled),
                entry.check_interval,
                entry.sensor_prompt,
                entry.action_prompt,
                entry.autonomy_level,
                json.dumps(entry.skills_allowed),
                entry.last_run_at,
                entry.updated_at,
                json.dumps(entry.metadata, ensure_ascii=False),
                goal_id,
            ),
        )
        self._db.conn.commit()
        self._log.info("goal_updated", goal_id=goal_id)
        return entry

    def touch_last_run(self, goal_id: str) -> None:
        """Update last_run_at to now."""
        now = datetime.now(timezone.utc).isoformat()
        self._db.conn.execute(
            "UPDATE goals SET last_run_at = ?, updated_at = ? WHERE id = ?",
            (now, now, goal_id),
        )
        self._db.conn.commit()

    def delete(self, goal_id: str) -> bool:
        cursor = self._db.conn.execute(
            "DELETE FROM goals WHERE id = ?", (goal_id,)
        )
        self._db.conn.commit()
        deleted = cursor.rowcount > 0
        if deleted:
            self._log.info("goal_deleted", goal_id=goal_id)
        return deleted

    # -- Goal runs --

    def record_run(
        self,
        goal_id: str,
        status: str,
        observation: str | None = None,
        decision: str | None = None,
        session_id: str | None = None,
        result: str | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> GoalRunEntry:
        now = datetime.now(timezone.utc).isoformat()
        finished = now if status != "running" else None
        entry = GoalRunEntry(
            id=uuid.uuid4().hex[:12],
            goal_id=goal_id,
            started_at=now,
            finished_at=finished,
            status=status,
            observation=observation,
            decision=decision,
            result=result,
            session_id=session_id,
            metadata=metadata or {},
        )
        self._db.conn.execute(
            """INSERT INTO goal_runs
               (id, goal_id, started_at, finished_at, status, observation,
                decision, result, session_id, metadata)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            (
                entry.id,
                entry.goal_id,
                entry.started_at,
                entry.finished_at,
                entry.status,
                entry.observation,
                entry.decision,
                entry.result,
                entry.session_id,
                json.dumps(entry.metadata, ensure_ascii=False),
            ),
        )
        self._db.conn.commit()
        self._log.info(
            "goal_run_recorded",
            goal_id=goal_id,
            run_id=entry.id,
            status=status,
        )
        return entry

    def get_runs(self, goal_id: str, limit: int = 20) -> list[GoalRunEntry]:
        rows = self._db.conn.execute(
            """SELECT * FROM goal_runs
               WHERE goal_id = ?
               ORDER BY started_at DESC LIMIT ?""",
            (goal_id, limit),
        ).fetchall()
        return [self._row_to_run(r) for r in rows]


class GoalLoader:
    """Load goals from file-based definitions and sync them to the DB.

    Directory layout::

        goals_dir/
          <name>/
            goal.json      # required: name, description, check_interval, ...
            sensor.md      # required: sensor prompt (multi-line)
            action.md      # optional: action prompt (multi-line)

    File definitions are the source of truth for *configuration*.
    The DB tracks *runtime state* (last_run_at, enabled, run history).
    """

    def __init__(self, goals_dir: str | Path) -> None:
        self.goals_dir = Path(goals_dir)
        self._log = log.bind(component="goal_loader", goals_dir=str(goals_dir))

    def load_all(self) -> list[dict[str, Any]]:
        """Load all goal definitions from disk. Returns list of raw dicts."""
        if not self.goals_dir.exists():
            self._log.debug("goals_dir_not_found")
            return []

        definitions: list[dict[str, Any]] = []
        for goal_path in sorted(self.goals_dir.iterdir()):
            if not goal_path.is_dir():
                continue
            defn = self._load_one(goal_path)
            if defn:
                definitions.append(defn)

        self._log.info("goals_loaded_from_disk", count=len(definitions))
        return definitions

    def _load_one(self, goal_path: Path) -> dict[str, Any] | None:
        """Load a single goal definition from a directory."""
        goal_json = goal_path / "goal.json"
        sensor_md = goal_path / "sensor.md"

        if not goal_json.exists():
            self._log.debug("skipping_no_goal_json", path=str(goal_path))
            return None

        if not sensor_md.exists():
            self._log.warning("skipping_no_sensor_md", path=str(goal_path))
            return None

        try:
            config = json.loads(goal_json.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError) as exc:
            self._log.error("goal_json_error", path=str(goal_json), error=str(exc))
            return None

        config["sensor_prompt"] = sensor_md.read_text(encoding="utf-8").strip()

        action_md = goal_path / "action.md"
        if action_md.exists():
            config["action_prompt"] = action_md.read_text(encoding="utf-8").strip()

        # Use directory name as fallback for name
        config.setdefault("name", goal_path.name)
        config.setdefault("description", config["name"])
        config.setdefault("check_interval", 300)
        config.setdefault("autonomy_level", "auto")
        config.setdefault("skills_allowed", [])
        config.setdefault("enabled", True)
        config["_source_dir"] = str(goal_path)

        return config

    def sync_to_store(self, store: GoalStore) -> dict[str, str]:
        """Load goals from disk and sync to DB.

        - New goals (by name) are inserted.
        - Existing goals have their config updated (prompts, interval, etc.)
          but runtime state (last_run_at, enabled) is preserved.

        Returns a dict of {name: "created" | "updated" | "unchanged"}.
        """
        definitions = self.load_all()
        existing = {g.name: g for g in store.list_all()}
        results: dict[str, str] = {}

        _KNOWN_KEYS = {
            "name", "description", "check_interval", "sensor_prompt",
            "action_prompt", "autonomy_level", "skills_allowed", "enabled",
            "_source_dir",
        }

        for defn in definitions:
            name = defn["name"]
            # Extract extra keys as metadata (e.g. always_act)
            extra = {k: v for k, v in defn.items() if k not in _KNOWN_KEYS}

            if name in existing:
                goal = existing[name]
                changed = False
                updates: dict[str, Any] = {}

                for field, db_field in [
                    ("description", "description"),
                    ("check_interval", "check_interval"),
                    ("sensor_prompt", "sensor_prompt"),
                    ("action_prompt", "action_prompt"),
                    ("autonomy_level", "autonomy_level"),
                    ("skills_allowed", "skills_allowed"),
                ]:
                    new_val = defn.get(field)
                    old_val = getattr(goal, db_field, None)
                    if new_val is not None and new_val != old_val:
                        updates[db_field] = new_val
                        changed = True

                if extra and extra != goal.metadata:
                    updates["metadata"] = extra
                    changed = True

                if changed:
                    store.update(goal.id, **updates)
                    results[name] = "updated"
                    self._log.info("goal_synced_updated", name=name)
                else:
                    results[name] = "unchanged"
            else:
                store.add(
                    name=name,
                    description=defn.get("description", name),
                    sensor_prompt=defn["sensor_prompt"],
                    action_prompt=defn.get("action_prompt", ""),
                    check_interval=defn.get("check_interval", 300),
                    autonomy_level=defn.get("autonomy_level", "auto"),
                    skills_allowed=defn.get("skills_allowed"),
                    metadata=extra or None,
                )
                results[name] = "created"
                self._log.info("goal_synced_created", name=name)

        return results

    @staticmethod
    def scaffold(goals_dir: str | Path, name: str) -> Path:
        """Create an empty goal directory with template files.

        Returns the path to the created goal directory.
        """
        goal_path = Path(goals_dir) / name
        goal_path.mkdir(parents=True, exist_ok=True)

        goal_json = goal_path / "goal.json"
        if not goal_json.exists():
            goal_json.write_text(
                json.dumps(
                    {
                        "name": name,
                        "description": f"TODO: describe what '{name}' monitors",
                        "check_interval": 300,
                        "autonomy_level": "auto",
                        "skills_allowed": [],
                        "enabled": True,
                    },
                    indent=4,
                )
                + "\n",
                encoding="utf-8",
            )

        sensor_md = goal_path / "sensor.md"
        if not sensor_md.exists():
            sensor_md.write_text(
                "TODO: Describe what the agent should observe/check.\n\n"
                "Example:\n"
                "Check Ariba for new purchase requisitions created in the last 15 minutes.\n"
                "Use the `get_ariba_data` skill to fetch recent items.\n",
                encoding="utf-8",
            )

        action_md = goal_path / "action.md"
        if not action_md.exists():
            action_md.write_text(
                "TODO: Describe what action to take when the sensor detects something.\n\n"
                "Example:\n"
                "Classify each new PR and send a summary to Slack.\n",
                encoding="utf-8",
            )

        return goal_path
