from __future__ import annotations

import json
import os
import subprocess
import threading
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen

from http.server import ThreadingHTTPServer

from shared_agent_memory import MemoryEngine, MemoryService
from shared_agent_memory.http_api import build_handler
from shared_agent_memory.schema import SCHEMA_VERSION


def make_service(tmp_path: Path) -> MemoryService:
    return MemoryService(str(tmp_path / "memory.sqlite3"))


def _request_json(url: str, *, method: str = "GET", payload: dict | None = None) -> tuple[int, dict]:
    body = None if payload is None else json.dumps(payload).encode("utf-8")
    request = Request(url, data=body, method=method)
    if body is not None:
        request.add_header("Content-Type", "application/json")
    with urlopen(request) as response:  # noqa: S310 - local test server only
        return response.status, json.loads(response.read().decode("utf-8"))


def _start_api(service: MemoryService) -> tuple[ThreadingHTTPServer, str]:
    server = ThreadingHTTPServer(("127.0.0.1", 0), build_handler(service))
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    host, port = server.server_address
    return server, f"http://{host}:{port}"


def test_manual_profile_fact_is_immediately_searchable(tmp_path: Path) -> None:
    service = make_service(tmp_path)
    service.upsert_profile_fact("Communication", "Sebas prefers concise, direct communication.")

    results = service.search("concise communication", scopes=["global"])

    assert results["results"]
    assert results["results"][0]["memory"]["type"] == "profile"
    assert results["results"][0]["memory"]["schema_version"] == SCHEMA_VERSION


def test_captured_conversation_stays_inbox_until_consolidated(tmp_path: Path) -> None:
    service = make_service(tmp_path)
    engine = MemoryEngine(service)

    service.capture_conversation("chat-1", "project", "We agreed to keep one active machine at a time.")
    processed = engine.process_pending()

    memory_id = processed[0]["memory_id"]
    memory = service.get_memory(memory_id)
    assert memory["status"] == "inbox"

    search_without_inbox = service.search("active machine", scopes=["project"])
    assert search_without_inbox["results"] == []

    promoted = service.consolidate([memory_id])
    assert promoted["promoted"] == [memory_id]

    search_with_active = service.search("active machine", scopes=["project"])
    assert search_with_active["results"][0]["memory"]["id"] == memory_id


def test_context_bundle_combines_global_project_and_task_artifacts(tmp_path: Path) -> None:
    service = make_service(tmp_path)
    service.create_project("proj_alpha", "Alpha")
    service.create_repo("repo_alpha", "Repo Alpha", project_id="proj_alpha")
    service.upsert_profile_fact("Preferences", "Sebas wants agents to preserve clarity.")
    service.create_memory(
        {
            "type": "project",
            "scope": "project",
            "project_id": "proj_alpha",
            "repo_id": "repo_alpha",
            "title": "Project goal",
            "content": "Alpha shares memory across projects.",
        }
    )
    service.create_memory(
        {
            "type": "decision",
            "scope": "project",
            "project_id": "proj_alpha",
            "repo_id": "repo_alpha",
            "title": "Storage engine",
            "content": "Use SQLite as canonical store.",
        }
    )
    service.create_memory(
        {
            "type": "artifact",
            "subtype": "research_artifact",
            "scope": "project",
            "project_id": "proj_alpha",
            "repo_id": "repo_alpha",
            "task_id": "task_alpha",
            "title": "Task note",
            "content": "Relevant artifact.",
        }
    )

    bundle = service.context_for(project="proj_alpha", repo="repo_alpha", task="task_alpha")

    assert bundle["profile_facts"]
    assert bundle["project_memories"]
    assert bundle["active_decisions"]
    assert bundle["task_relevant_artifacts"]


def test_export_import_preserves_operational_records_without_approvals_or_handoffs(tmp_path: Path) -> None:
    service = make_service(tmp_path)
    service.create_project("proj_alpha", "Alpha")
    service.create_repo("repo_alpha", "Repo Alpha", project_id="proj_alpha")
    task = service.create_task(
        task_id="task_alpha",
        title="Resume V2 task",
        intent="Need durable task state.",
        project_id="proj_alpha",
        repo_id="repo_alpha",
        owner_agent="personal-agent",
        run_id="run_alpha",
    )
    service.start_task_run(task["id"], "personal-agent", input_payload={"mode": "runner"})
    service.create_artifact(
        task_id=task["id"],
        artifact_type="brief",
        title="Brief",
        content="Keep resumable state in shared DB.",
        source_ref="personal-agent",
    )

    exported = service.export()

    second = MemoryService(str(tmp_path / "second.sqlite3"))
    second.import_data(exported)
    bundle = second.task_bundle(task["id"])

    assert "approvals" not in bundle
    assert "handoffs" not in bundle
    assert bundle["task"]["run_id"] == "run_alpha"
    assert bundle["runs"]
    assert bundle["artifacts"]


def test_list_memories_can_filter_by_v2_facets(tmp_path: Path) -> None:
    service = make_service(tmp_path)
    service.create_memory(
        {
            "id": "run_123",
            "type": "episode",
            "subtype": "research_run",
            "scope": "global",
            "title": "Research run",
            "content": "Goal: inspect",
            "run_id": "run-123",
            "origin_agent": "personal-agent",
        }
    )
    service.create_memory(
        {
            "type": "artifact",
            "subtype": "research_source",
            "scope": "global",
            "title": "Source",
            "content": "source",
            "run_id": "run-123",
            "url": "https://example.com/spec",
            "domain": "example.com",
            "origin_agent": "personal-agent",
        }
    )

    matches = service.list_memories(subtype="research_source", run_id="run-123", domain="example.com", origin_agent="personal-agent")

    assert len(matches) == 1
    assert matches[0]["url"] == "https://example.com/spec"


def test_search_works_without_embeddings(tmp_path: Path) -> None:
    service = make_service(tmp_path)
    service.create_memory(
        {
            "type": "artifact",
            "subtype": "research_claim",
            "scope": "global",
            "title": "Claim",
            "content": "SQLite remains optional for embeddings.",
            "embedding": None,
        }
    )

    results = service.search("optional embeddings", scopes=["global"])

    assert results["results"]
    assert results["results"][0]["memory"]["subtype"] == "research_claim"


def test_http_api_supports_status_ingest_and_search(tmp_path: Path) -> None:
    service = make_service(tmp_path)
    server, base_url = _start_api(service)
    try:
        status_code, status_payload = _request_json(f"{base_url}/api/status")
        assert status_code == 200
        assert status_payload["ok"] is True

        created_code, created_payload = _request_json(
            f"{base_url}/api/ingest",
            method="POST",
            payload={
                "id": "mem_http_1",
                "type": "artifact",
                "subtype": "research_claim",
                "scope": "global",
                "title": "HTTP memory",
                "content": "Remote API keeps shared memory reachable.",
                "status": "active",
            },
        )
        assert created_code == 201
        assert created_payload["id"] == "mem_http_1"

        search_code, search_payload = _request_json(f"{base_url}/api/search?q=reachable&scope=global")
        assert search_code == 200
        assert search_payload["results"]
        assert search_payload["results"][0]["memory"]["id"] == "mem_http_1"
    finally:
        server.shutdown()
        server.server_close()


def test_http_api_supports_tasks_runs_and_bundles(tmp_path: Path) -> None:
    service = make_service(tmp_path)
    server, base_url = _start_api(service)
    try:
        task_code, task = _request_json(
            f"{base_url}/api/tasks",
            method="POST",
            payload={
                "task_id": "task_http_1",
                "title": "Remote task",
                "intent": "Keep task state remote.",
                "owner_agent": "personal-agent",
            },
        )
        assert task_code == 201
        assert task["id"] == "task_http_1"

        run_code, run = _request_json(
            f"{base_url}/api/task-runs",
            method="POST",
            payload={"task_id": task["id"], "agent_id": "personal-agent", "input_payload": {"mode": "remote"}},
        )
        assert run_code == 201
        assert run["task_id"] == task["id"]

        patch_code, patched = _request_json(
            f"{base_url}/api/tasks/{task['id']}",
            method="PATCH",
            payload={"status": "in_progress", "run_id": run["id"]},
        )
        assert patch_code == 200
        assert patched["status"] == "in_progress"
        assert patched["run_id"] == run["id"]

        bundle_code, bundle = _request_json(f"{base_url}/api/tasks/{task['id']}/bundle")
        assert bundle_code == 200
        assert bundle["task"]["id"] == task["id"]
        assert bundle["runs"][0]["id"] == run["id"]
    finally:
        server.shutdown()
        server.server_close()


def test_http_api_supports_memory_lookup_by_id(tmp_path: Path) -> None:
    service = make_service(tmp_path)
    service.create_memory(
        {
            "id": "mem_lookup_1",
            "type": "artifact",
            "scope": "global",
            "title": "Lookup memory",
            "content": "Direct id lookup should work.",
        }
    )
    server, base_url = _start_api(service)
    try:
        status_code, payload = _request_json(f"{base_url}/api/memories/mem_lookup_1")
        assert status_code == 200
        assert payload["id"] == "mem_lookup_1"
    finally:
        server.shutdown()
        server.server_close()


def test_http_api_rejects_search_without_query(tmp_path: Path) -> None:
    service = make_service(tmp_path)
    server, base_url = _start_api(service)
    try:
        try:
            _request_json(f"{base_url}/api/search")
        except HTTPError as exc:
            assert exc.code == 400
        else:
            raise AssertionError("Expected HTTP 400 for missing query")
    finally:
        server.shutdown()
        server.server_close()


def test_http_api_rejects_search_by_id(tmp_path: Path) -> None:
    service = make_service(tmp_path)
    service.create_memory(
        {
            "id": "mem_lookup_1",
            "type": "artifact",
            "scope": "global",
            "title": "Lookup memory",
            "content": "Direct id lookup should work.",
        }
    )
    server, base_url = _start_api(service)
    try:
        try:
            _request_json(f"{base_url}/api/search?id=mem_lookup_1")
        except HTTPError as exc:
            assert exc.code == 400
        else:
            raise AssertionError("Expected HTTP 400 for search by id")
    finally:
        server.shutdown()
        server.server_close()


def test_maintenance_jobs_exist_by_default(tmp_path: Path) -> None:
    service = make_service(tmp_path)

    jobs = {job["id"] for job in service.list_maintenance_jobs()}

    assert "job_consolidation_daily_10" in jobs
    assert "job_consolidation_daily_15" in jobs
    assert "job_consolidation_weekly" in jobs


def test_maintenance_tick_records_run(tmp_path: Path) -> None:
    service = make_service(tmp_path)
    engine = MemoryEngine(service)

    now = "2026-03-20T10:00:00+00:00"
    service.update_maintenance_job("job_consolidation_daily_10", next_due_at=now)
    results = engine.run_due_maintenance(job_id="job_consolidation_daily_10")

    assert results
    job = service.get_maintenance_job("job_consolidation_daily_10")
    runs = service.list_maintenance_runs(job_id="job_consolidation_daily_10")

    assert job["last_status"] == "completed"
    assert runs


def test_service_startup_runs_due_maintenance(tmp_path: Path) -> None:
    db_path = tmp_path / "memory.sqlite3"
    service = MemoryService(str(db_path))
    now = "2026-03-20T10:00:00+00:00"

    service.update_maintenance_job("job_consolidation_daily_10", next_due_at=now)
    restarted = MemoryService(str(db_path))
    job = restarted.get_maintenance_job("job_consolidation_daily_10")
    runs = restarted.list_maintenance_runs(job_id="job_consolidation_daily_10")

    assert job["last_status"] == "completed"
    assert runs


def test_write_operations_trigger_due_maintenance_check(tmp_path: Path) -> None:
    service = make_service(tmp_path)
    now = "2026-03-20T10:00:00+00:00"

    service.update_maintenance_job("job_consolidation_daily_10", next_due_at=now)
    service.create_project("proj_alpha", "Alpha")

    job = service.get_maintenance_job("job_consolidation_daily_10")
    runs = service.list_maintenance_runs(job_id="job_consolidation_daily_10")

    assert job["last_status"] == "completed"
    assert runs


def test_high_confidence_inbox_memory_auto_promotes_on_write(tmp_path: Path) -> None:
    service = make_service(tmp_path)
    service.update_maintenance_job("job_consolidation_daily_10", next_due_at="2026-03-20T10:00:00+00:00")

    memory = service.create_memory(
        {
            "id": "mem_auto_promote",
            "type": "episode",
            "scope": "project",
            "status": "inbox",
            "title": "Auto promote me",
            "content": "A durable session note that should become active automatically.",
            "confidence": 0.95,
            "freshness": 0.9,
        }
    )

    assert memory["status"] == "active"


def test_conflicts_only_for_selected_types(tmp_path: Path) -> None:
    service = make_service(tmp_path)
    service.create_memory(
        {
            "id": "dec_active",
            "type": "decision",
            "scope": "project",
            "status": "active",
            "title": "Decision A",
            "content": "Feature X is enabled for launch.",
        }
    )
    service.create_memory(
        {
            "id": "dec_inbox",
            "type": "decision",
            "scope": "project",
            "status": "inbox",
            "title": "Decision B",
            "content": "Feature X is not enabled for launch.",
        }
    )
    service.consolidate_candidates(
        config={
            "candidate_age_days": 0,
            "max_candidates": 10,
            "conflict_threshold": 0.1,
            "dedupe_high_threshold": 0.99,
            "dedupe_mid_threshold": 0.99,
            "promote_confidence": 1.0,
            "llm_enabled": False,
        }
    )
    with service.store.connection() as conn:
        conflicts = conn.execute("SELECT COUNT(*) AS count FROM memory_conflicts").fetchone()["count"]

    assert conflicts == 1


def test_run_maintenance_daemon_wrapper_runs_from_repo_checkout(tmp_path: Path) -> None:
    db_path = tmp_path / "memory.sqlite3"
    service = MemoryService(str(db_path))
    service.update_maintenance_job("job_consolidation_daily_10", next_due_at="2026-03-20T10:00:00+00:00")

    script = Path(__file__).resolve().parents[1] / "scripts" / "run-maintenance-daemon.sh"
    env = os.environ.copy()
    env["DB_PATH"] = str(db_path)
    env["INTERVAL_SECONDS"] = "1"
    env["JITTER_SECONDS"] = "0"
    result = subprocess.run(
        [str(script), "--once", "--job-id", "job_consolidation_daily_10"],
        capture_output=True,
        text=True,
        check=True,
        env=env,
    )

    restarted = MemoryService(str(db_path))
    job = restarted.get_maintenance_job("job_consolidation_daily_10")
    runs = restarted.list_maintenance_runs(job_id="job_consolidation_daily_10")

    assert '"status": "completed"' in result.stdout
    assert job["last_status"] == "completed"
    assert runs


def test_versioned_maintenance_systemd_service_exists() -> None:
    service_file = Path(__file__).resolve().parents[1] / "systemd" / "user" / "agents-database-maintenance.service"
    content = service_file.read_text()

    assert service_file.exists()
    assert "ExecStart=/mnt/rpi/agents-database/scripts/run-maintenance-daemon.sh" in content


def test_migrate_v2_rewrites_legacy_rows_and_drops_legacy_tables(tmp_path: Path) -> None:
    service = make_service(tmp_path)
    with service.store.connection() as conn:
        conn.execute(
            """
            INSERT INTO memories (
                id, type, scope, status, source_kind, title, content, summary, confidence, freshness,
                created_at, updated_at, observed_at, source_ref, evidence_ref, embedding_json, metadata_json
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (
                "legacy_run",
                "episode",
                "global",
                "active",
                "run",
                "Research run: Legacy cleanup",
                "Goal: Legacy cleanup\nScope: Repo\nAssumptions: None\nSummary: Pending",
                "Pending",
                0.9,
                0.8,
                "2026-03-19T00:00:00+00:00",
                "2026-03-19T00:00:00+00:00",
                "2026-03-19T00:00:00+00:00",
                "personal-agent:run:run-123",
                "personal-agent:run:run-123",
                None,
                service.store.dumps({"legacy_system": "personal-agent", "legacy_kind": "research_run", "legacy_run_id": "run-123"}),
            ),
        )
        conn.execute(
            """
            INSERT INTO tasks (
                id, title, intent, kind, status, priority, created_at, updated_at, metadata_json
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (
                "task_legacy",
                "Legacy task",
                "migrate me",
                "task",
                "open",
                3,
                "2026-03-19T00:00:00+00:00",
                "2026-03-19T00:00:00+00:00",
                service.store.dumps({"legacy_kind": "task", "legacy_run_id": "run-123"}),
            ),
        )
        conn.execute(
            """
            CREATE TABLE approvals (
                id TEXT PRIMARY KEY,
                task_id TEXT NOT NULL,
                kind TEXT NOT NULL,
                status TEXT NOT NULL,
                risk_level TEXT NOT NULL,
                payload_json TEXT NOT NULL,
                resolution_note TEXT,
                requested_at TEXT NOT NULL,
                resolved_at TEXT,
                created_at TEXT NOT NULL,
                updated_at TEXT NOT NULL
            )
            """
        )
        conn.execute(
            """
            INSERT INTO approvals (
                id, task_id, kind, status, risk_level, payload_json, resolution_note, requested_at, resolved_at, created_at, updated_at
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (
                "approval_legacy",
                "task_legacy",
                "merge",
                "pending",
                "medium",
                service.store.dumps({"kind": "merge"}),
                None,
                "2026-03-19T00:00:00+00:00",
                None,
                "2026-03-19T00:00:00+00:00",
                "2026-03-19T00:00:00+00:00",
            ),
        )

    service.migrate_v2()

    with service.store.connection() as conn:
        legacy_memory = conn.execute("SELECT * FROM memories WHERE id = 'legacy_run'").fetchone()
        legacy_task = conn.execute("SELECT * FROM tasks WHERE id = 'task_legacy'").fetchone()
        approvals_table = conn.execute(
            "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'approvals'"
        ).fetchone()

    assert legacy_memory is not None
    assert legacy_task is not None
    assert approvals_table is None
