import json
import sqlite3
from datetime import UTC, datetime, timedelta
from typing import Any, cast

from ..models import AgentRegistration, JobObservation, RemoteEvent, RunDescriptor
from ..timeutil import iso, parse_iso


def _json(value: Any) -> str:
    return json.dumps(value, ensure_ascii=False, default=str, separators=(",", ":"))


class WatcherRepository:
    def __init__(self, connection: sqlite3.Connection) -> None:
        self.connection = connection

    def ensure_job(self, observation: JobObservation, now: datetime) -> int:
        job = observation.job
        self.connection.execute(
            """INSERT INTO jobs(job_key,name,description,source_type,job_type,enabled,expected_interval_seconds,
               timeout_seconds,stale_after_seconds,created_at,updated_at,metadata_json)
               VALUES(?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(job_key) DO UPDATE SET name=excluded.name,
               description=excluded.description,updated_at=excluded.updated_at,metadata_json=excluded.metadata_json""",
            (job.job_key, job.name, job.description, job.source_type, job.job_type, int(job.enabled),
             job.expected_interval_seconds, job.timeout_seconds, job.stale_after_seconds, iso(now), iso(now), _json(job.metadata)),
        )
        row = self.connection.execute("SELECT id FROM jobs WHERE job_key=?", (job.job_key,)).fetchone()
        assert row is not None
        return int(row[0])

    def ensure_agent(self, registration: AgentRegistration, now: datetime) -> int:
        self.connection.execute(
            """INSERT INTO execution_agents(agent_key,agent_name,agent_version,environment,host,runtime,
               capabilities_json,status,first_seen_at,last_seen_at,last_heartbeat_at,created_at,updated_at,metadata_json)
               VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(agent_key) DO UPDATE SET agent_name=excluded.agent_name,
               agent_version=excluded.agent_version,environment=excluded.environment,host=excluded.host,runtime=excluded.runtime,
               capabilities_json=excluded.capabilities_json,status='online',last_seen_at=excluded.last_seen_at,
               last_heartbeat_at=excluded.last_heartbeat_at,updated_at=excluded.updated_at,metadata_json=excluded.metadata_json""",
            (registration.agent_key, registration.agent_name, registration.agent_version, registration.environment,
             registration.host, registration.runtime, _json(registration.capabilities), "online", iso(now), iso(now),
             iso(now), iso(now), iso(now), _json(registration.metadata)),
        )
        row = self.connection.execute("SELECT id FROM execution_agents WHERE agent_key=?", (registration.agent_key,)).fetchone()
        assert row is not None
        return int(row[0])

    def record_remote_event(self, event: RemoteEvent, agent_id: int, received_at: datetime) -> tuple[int, bool]:
        try:
            cursor = self.connection.execute(
                """INSERT INTO remote_events(agent_id,event_id,worker_key,job_key,event_type,sequence,occurred_at,
                   received_at,payload_json) VALUES(?,?,?,?,?,?,?,?,?)""",
                (agent_id, event.event_id, event.worker_key, event.job_key, event.event_type, event.sequence,
                 iso(event.occurred_at), iso(received_at), _json(event.payload)),
            )
        except sqlite3.IntegrityError as exc:
            if "remote_events.agent_id, remote_events.event_id" not in str(exc):
                raise
            row = self.connection.execute("SELECT id FROM remote_events WHERE agent_id=? AND event_id=?",
                                          (agent_id, event.event_id)).fetchone()
            assert row is not None
            return int(row[0]), False
        assert cursor.lastrowid is not None
        return int(cursor.lastrowid), True

    def mark_remote_event(self, event_id: int, processed: bool, error: str | None = None) -> None:
        self.connection.execute("UPDATE remote_events SET processed=?,processing_error=? WHERE id=?",
                                (int(processed), error, event_id))

    def ensure_instance(self, observation: JobObservation, now: datetime) -> tuple[int, bool]:
        instance = observation.instance
        job_id = self.ensure_job(observation, now)
        existing = self.connection.execute("SELECT id FROM job_instances WHERE instance_key=?", (instance.instance_key,)).fetchone()
        new = existing is None
        agent_id = None
        if instance.agent_key:
            agent_row = self.connection.execute("SELECT id FROM execution_agents WHERE agent_key=?", (instance.agent_key,)).fetchone()
            agent_id = int(agent_row[0]) if agent_row else None
        worker_key = instance.worker_key or instance.instance_key
        self.connection.execute(
            """INSERT INTO job_instances(job_id,instance_key,host,instance_name,runtime_type,service_name,
               container_name,process_name,source_reference,enabled,first_seen_at,last_seen_at,created_at,updated_at,metadata_json,
               worker_key,agent_id,location_type,location_name)
               VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(instance_key) DO UPDATE SET last_seen_at=excluded.last_seen_at,
               updated_at=excluded.updated_at,enabled=excluded.enabled,metadata_json=excluded.metadata_json,worker_key=excluded.worker_key,
               agent_id=excluded.agent_id,location_type=excluded.location_type,location_name=excluded.location_name""",
            (job_id, instance.instance_key, instance.host, instance.instance_name, instance.runtime_type,
             instance.service_name, instance.container_name, instance.process_name, instance.source_reference,
             int(instance.enabled), iso(now), iso(now), iso(now), iso(now), _json(instance.metadata), worker_key, agent_id,
             instance.location_type, instance.location_name),
        )
        row = self.connection.execute("SELECT id FROM job_instances WHERE instance_key=?", (instance.instance_key,)).fetchone()
        assert row is not None
        return int(row[0]), new

    def ensure_run(self, instance_id: int, run: RunDescriptor | None, observation: JobObservation, now: datetime) -> int | None:
        if run is None:
            return None
        existing = None
        if run.external_run_id is not None:
            existing = self.connection.execute(
                "SELECT id FROM job_runs WHERE instance_id=? AND external_run_id=?", (instance_id, run.external_run_id)
            ).fetchone()
        elif run.started_at is not None:
            existing = self.connection.execute(
                "SELECT id FROM job_runs WHERE instance_id=? AND external_run_id IS NULL AND started_at=?",
                (instance_id, iso(run.started_at)),
            ).fetchone()
        params = (instance_id, run.external_run_id, iso(run.started_at), iso(run.finished_at),
                  observation.operational_state.value, observation.health_status.value, run.exit_code,
                  run.runtime_seconds, run.records_processed, run.error_message, iso(now), iso(now), _json({}))
        if existing:
            self.connection.execute(
                """UPDATE job_runs SET finished_at=?,operational_state=?,health_status=?,exit_code=?,runtime_seconds=?,
                   records_processed=?,error_message=?,updated_at=? WHERE id=?""",
                (iso(run.finished_at), observation.operational_state.value, observation.health_status.value, run.exit_code,
                 run.runtime_seconds, run.records_processed, run.error_message, iso(now), int(existing[0])),
            )
            return int(existing[0])
        cursor = self.connection.execute(
            """INSERT INTO job_runs(instance_id,external_run_id,started_at,finished_at,operational_state,health_status,
               exit_code,runtime_seconds,records_processed,error_message,created_at,updated_at,metadata_json)
               VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)""", params)
        assert cursor.lastrowid is not None
        return int(cursor.lastrowid)

    def current(self, instance_id: int) -> sqlite3.Row | None:
        return cast(sqlite3.Row | None, self.connection.execute("SELECT * FROM job_status_current WHERE instance_id=?", (instance_id,)).fetchone())

    def persist_observation(self, observation: JobObservation, collector_run_id: int | None, now: datetime) -> tuple[int, bool]:
        instance_id, new_instance = self.ensure_instance(observation, now)
        run_id = self.ensure_run(instance_id, observation.run, observation, now)
        evidence = [{"key": item.key, "value": item.value, "source": item.source,
                     "observed_at": iso(item.observed_at), "description": item.description}
                    for item in observation.evidence]
        cursor = self.connection.execute(
            """INSERT INTO job_observations(instance_id,run_id,collector_run_id,observed_at,operational_state,health_status,
               observation_status,severity,status_reason,last_activity_at,expected_next_run_at,heartbeat_at,exit_code,
               evidence_json,raw_payload_json,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
            (instance_id, run_id, collector_run_id, iso(observation.observed_at), observation.operational_state.value,
             observation.health_status.value, observation.observation_status.value, observation.severity.value,
             observation.status_reason, iso(observation.last_activity_at), iso(observation.expected_next_run_at),
             iso(observation.heartbeat_at), observation.exit_code, _json(evidence), _json(observation.raw_payload), iso(now)),
        )
        assert cursor.lastrowid is not None
        observation_id = int(cursor.lastrowid)
        previous = self.current(instance_id)
        unavailable_with_previous = observation.observation_status.value == "unavailable" and previous is not None
        retained = previous if unavailable_with_previous else None
        effective_operational = retained["operational_state"] if retained else observation.operational_state.value
        effective_health = retained["health_status"] if retained else observation.health_status.value
        effective_severity = retained["severity"] if retained else observation.severity.value
        effective_reason = ("last known job status retained; current observation unavailable" if unavailable_with_previous
                            else observation.status_reason)
        changed = previous is None or any(previous[key] != value for key, value in {
            "operational_state": effective_operational,
            "health_status": effective_health,
            "observation_status": observation.observation_status.value,
            "severity": effective_severity,
        }.items()) or (not unavailable_with_previous and (previous["last_error"] or None) != (observation.run.error_message if observation.run else None))
        last_success_at = previous["last_success_at"] if previous else None
        last_failure_at = previous["last_failure_at"] if previous else None
        last_started_at = previous["last_started_at"] if previous else None
        last_finished_at = previous["last_finished_at"] if previous else None
        if observation.run:
            last_started_at = iso(observation.run.started_at) or last_started_at
            last_finished_at = iso(observation.run.finished_at) or last_finished_at
            if effective_health == "healthy" and observation.run.finished_at:
                last_success_at = iso(observation.run.finished_at)
            if effective_health == "failed":
                last_failure_at = iso(observation.run.finished_at or observation.observed_at)
        retained_run_id = retained["run_id"] if retained is not None else run_id
        current_values = (instance_id, retained_run_id, effective_operational, effective_health,
                          observation.observation_status.value, effective_severity, effective_reason,
                          iso(observation.observed_at), (retained["last_activity_at"] if retained else iso(observation.last_activity_at)), last_started_at, last_finished_at,
                          last_success_at, last_failure_at, iso(observation.expected_next_run_at), iso(observation.heartbeat_at),
                          (retained["exit_code"] if retained else observation.exit_code),
                          (retained["last_error"] if retained else (observation.run.error_message if observation.run else None)),
                          observation.instance.job.job_key, iso(now), _json(evidence))
        self.connection.execute(
            """INSERT INTO job_status_current(instance_id,run_id,operational_state,health_status,observation_status,severity,
               status_reason,last_observed_at,last_activity_at,last_started_at,last_finished_at,last_success_at,last_failure_at,
               expected_next_run_at,heartbeat_at,exit_code,last_error,collector_name,updated_at,evidence_json)
               VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(instance_id) DO UPDATE SET run_id=excluded.run_id,
               operational_state=excluded.operational_state,health_status=excluded.health_status,observation_status=excluded.observation_status,
               severity=excluded.severity,status_reason=excluded.status_reason,last_observed_at=excluded.last_observed_at,
               last_activity_at=excluded.last_activity_at,last_started_at=excluded.last_started_at,last_finished_at=excluded.last_finished_at,
               last_success_at=excluded.last_success_at,last_failure_at=excluded.last_failure_at,expected_next_run_at=excluded.expected_next_run_at,
               heartbeat_at=excluded.heartbeat_at,exit_code=excluded.exit_code,last_error=excluded.last_error,collector_name=excluded.collector_name,
               updated_at=excluded.updated_at,evidence_json=excluded.evidence_json""", current_values)
        if changed:
            event_type = "new_instance" if previous is None else "status_changed"
            if previous and previous["health_status"] == "failed" and effective_health != "failed":
                event_type = "error_resolved"
            elif previous and previous["health_status"] != "failed" and effective_health == "failed":
                event_type = "error_raised"
            self.connection.execute(
                """INSERT INTO status_events(instance_id,run_id,event_type,previous_operational_state,new_operational_state,
                   previous_health_status,new_health_status,previous_observation_status,new_observation_status,previous_severity,
                   new_severity,reason,occurred_at,observation_id,metadata_json) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
                (instance_id, run_id, event_type, previous["operational_state"] if previous else None,
                 effective_operational, previous["health_status"] if previous else None, effective_health,
                 previous["observation_status"] if previous else None, observation.observation_status.value,
                 previous["severity"] if previous else None, effective_severity, effective_reason,
                 iso(observation.observed_at), observation_id, _json({})),
            )
        return observation_id, changed or new_instance

    def save_collector_run(self, result: Any, observations_created: int) -> int:
        errors = list(result.errors)
        cursor = self.connection.execute(
            """INSERT INTO collector_runs(collector_name,collector_version,started_at,finished_at,success,error_type,
               error_message,jobs_seen,instances_seen,observations_created,duration_ms,metadata_json) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)""",
            (result.collector_name, result.collector_version, iso(result.started_at), iso(result.finished_at), int(result.success),
             errors[0].error_type if errors else None, errors[0].message if errors else None,
             len({obs.job.job_key for obs in result.observations}), len({obs.instance.instance_key for obs in result.observations}),
             observations_created, int((result.finished_at - result.started_at).total_seconds() * 1000), _json(result.diagnostics)),
        )
        assert cursor.lastrowid is not None
        return int(cursor.lastrowid)

    def save_watcher_event(self, event_type: str, severity: str, message: str, component: str, metadata: dict[str, Any] | None = None) -> None:
        self.connection.execute("INSERT INTO watcher_events(event_type,severity,message,created_at,component,metadata_json) VALUES(?,?,?,?,?,?)",
                               (event_type, severity, message, iso(datetime.now(UTC)), component, _json(metadata or {})))

    def status_rows(self) -> list[sqlite3.Row]:
        return list(self.connection.execute("""SELECT c.*, i.instance_key, COALESCE(i.worker_key, i.instance_key) AS worker_key,
            i.host, i.runtime_type, i.source_reference, i.location_type, i.location_name, i.agent_id,
            j.job_key, j.name, j.job_type FROM job_status_current c JOIN job_instances i ON i.id=c.instance_id
            JOIN jobs j ON j.id=i.job_id ORDER BY j.job_key, i.instance_key"""))

    def worker_rows(self) -> list[sqlite3.Row]:
        return list(self.connection.execute("""SELECT COALESCE(i.worker_key, i.instance_key) AS worker_key,
            j.job_key, j.name, COUNT(*) AS instances_total, MAX(c.last_observed_at) AS last_observed_at,
            MIN(c.health_status) AS health_status, GROUP_CONCAT(DISTINCT i.location_type) AS locations
            FROM job_status_current c JOIN job_instances i ON i.id=c.instance_id JOIN jobs j ON j.id=i.job_id
            GROUP BY COALESCE(i.worker_key, i.instance_key), j.job_key, j.name ORDER BY j.job_key, worker_key"""))

    def agent_rows(self) -> list[sqlite3.Row]:
        return list(self.connection.execute("SELECT * FROM execution_agents ORDER BY agent_key"))

    def remote_event_rows(self, limit: int = 100) -> list[sqlite3.Row]:
        safe_limit = max(1, min(limit, 500))
        return list(self.connection.execute("SELECT * FROM remote_events ORDER BY received_at DESC LIMIT ?", (safe_limit,)))

    def refresh_remote_staleness(self, now: datetime, stale_after_seconds: int) -> int:
        changed = 0
        cutoff = now.timestamp() - stale_after_seconds
        agents = self.connection.execute("SELECT id,last_heartbeat_at,status FROM execution_agents").fetchall()
        for agent in agents:
            heartbeat = parse_iso(agent["last_heartbeat_at"])
            is_stale = heartbeat is None or heartbeat.timestamp() < cutoff
            if is_stale and agent["status"] != "stale":
                self.connection.execute("UPDATE execution_agents SET status='stale',updated_at=? WHERE id=?", (iso(now), agent["id"]))
            if not is_stale:
                continue
            current_rows = self.connection.execute("""SELECT c.*,i.id AS instance_id FROM job_status_current c
                JOIN job_instances i ON i.id=c.instance_id WHERE i.agent_id=? AND c.observation_status='fresh'""", (agent["id"],)).fetchall()
            for current in current_rows:
                self.connection.execute("""UPDATE job_status_current SET health_status='stale',observation_status='stale',
                    severity='warning',status_reason='remote agent heartbeat overdue',updated_at=? WHERE instance_id=?""",
                                        (iso(now), current["instance_id"]))
                self.connection.execute("""INSERT INTO status_events(instance_id,run_id,event_type,previous_operational_state,
                    new_operational_state,previous_health_status,new_health_status,previous_observation_status,new_observation_status,
                    previous_severity,new_severity,reason,occurred_at,metadata_json) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
                    (current["instance_id"], current["run_id"], "agent_stale", current["operational_state"], current["operational_state"],
                     current["health_status"], "stale", current["observation_status"], "stale", current["severity"], "warning",
                     "remote agent heartbeat overdue", iso(now), _json({"agent_id": agent["id"]})))
                changed += 1
        if changed or agents:
            self.connection.commit()
        return changed

    def events(self, limit: int = 50) -> list[sqlite3.Row]:
        safe_limit = max(1, min(limit, 500))
        return list(self.connection.execute("SELECT * FROM status_events ORDER BY occurred_at DESC LIMIT ?", (safe_limit,)))

    def collector_rows(self, limit: int = 50) -> list[sqlite3.Row]:
        safe_limit = max(1, min(limit, 500))
        return list(self.connection.execute("SELECT * FROM collector_runs ORDER BY started_at DESC LIMIT ?", (safe_limit,)))

    def summary(self) -> dict[str, int]:
        rows = self.connection.execute("SELECT health_status, COUNT(*) AS count FROM job_status_current GROUP BY health_status").fetchall()
        result = {"jobs_total": 0, "jobs_healthy": 0, "jobs_degraded": 0, "jobs_failed": 0, "jobs_stale": 0, "jobs_unknown": 0}
        for row in rows:
            key = f"jobs_{row['health_status']}"
            if key in result:
                result[key] = int(row["count"])
            result["jobs_total"] += int(row["count"])
        return result

    def retention(self, days: dict[str, int], dry_run: bool) -> dict[str, int]:
        now = datetime.now(UTC)
        tables = {"job_observations": ("observed_at", days["job_observations"]), "job_runs": ("created_at", days["job_runs"]),
                  "status_events": ("occurred_at", days["status_events"]), "collector_runs": ("started_at", days["collector_runs"]),
                  "watcher_events": ("created_at", days["watcher_events"])}
        result: dict[str, int] = {}
        for table, (column, age_days) in tables.items():
            cutoff = iso(now - timedelta(days=age_days))
            count = int(self.connection.execute(f"SELECT COUNT(*) FROM {table} WHERE {column} < ?", (cutoff,)).fetchone()[0])
            result[table] = count
            if not dry_run:
                self.connection.execute(f"DELETE FROM {table} WHERE {column} < ?", (cutoff,))
        if not dry_run:
            self.connection.commit()
        return result
