from __future__ import annotations

import os
import socket
import subprocess
import tempfile
import time
from pathlib import Path

import pytest

from kyrodb import AsyncKyroDBClient, KyroDBClient, exact


def _find_server_binary() -> Path | None:
    env_bin = os.environ.get("KYRODB_SERVER_BIN")
    if env_bin:
        path = Path(env_bin).expanduser().resolve()
        return path if path.exists() else None

    repo_root = None
    for parent in Path(__file__).resolve().parents:
        if (parent / "Cargo.toml").exists() and (parent / "engine" / "Cargo.toml").exists():
            repo_root = parent
            break
    if repo_root is None:
        return None

    candidates = [
        repo_root / "target" / "release" / "kyrodb_server",
        repo_root / "target" / "debug" / "kyrodb_server",
    ]
    for candidate in candidates:
        if candidate.exists():
            return candidate
    return None


def _pick_free_port() -> int:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.bind(("127.0.0.1", 0))
        return int(sock.getsockname()[1])


def _write_config(path: Path, *, port: int, http_port: int, data_dir: Path) -> None:
    path.write_text(
        (
            "server:\n"
            '  host: "127.0.0.1"\n'
            f"  port: {port}\n"
            f"  http_port: {http_port}\n"
            "auth:\n"
            "  enabled: false\n"
            "hnsw:\n"
            "  dimension: 4\n"
            "  distance: cosine\n"
            "persistence:\n"
            f'  data_dir: "{data_dir.as_posix()}"\n'
            "  snapshot_interval_mutations: 100\n"
        ),
        encoding="utf-8",
    )


@pytest.fixture(scope="module")
def live_target() -> str:
    binary = _find_server_binary()
    if binary is None:
        pytest.skip("KYRODB_SERVER_BIN not set and local kyrodb_server binary not found")

    with tempfile.TemporaryDirectory(prefix="kyrodb-python-integ-") as tmp:
        root = Path(tmp)
        data_dir = root / "data"
        data_dir.mkdir(parents=True, exist_ok=True)
        config_path = root / "config.yaml"
        log_path = root / "server.log"

        port = _pick_free_port()
        http_port = _pick_free_port()
        _write_config(config_path, port=port, http_port=http_port, data_dir=data_dir)

        with log_path.open("w", encoding="utf-8") as log_file:
            process = subprocess.Popen(
                [str(binary), "--config", str(config_path)],
                stdout=log_file,
                stderr=subprocess.STDOUT,
                cwd=str(root),
            )

            target = f"127.0.0.1:{port}"
            try:
                deadline = time.time() + 20
                ready = False
                while time.time() < deadline:
                    if process.poll() is not None:
                        break
                    try:
                        with socket.create_connection(("127.0.0.1", port), timeout=0.25):
                            ready = True
                            break
                    except OSError:
                        time.sleep(0.1)

                if not ready:
                    logs = log_path.read_text(encoding="utf-8")
                    pytest.fail(
                        "kyrodb_server failed to become ready within 20s.\n"
                        f"binary={binary}\n"
                        f"config={config_path}\n"
                        f"logs:\n{logs}"
                    )

                with KyroDBClient(target=target) as client:
                    client.wait_for_ready(timeout_s=5)

                yield target
            finally:
                process.terminate()
                try:
                    process.wait(timeout=8)
                except subprocess.TimeoutExpired:
                    process.kill()
                    process.wait(timeout=5)


@pytest.mark.integration
def test_sync_crud_search_with_live_server(live_target: str) -> None:
    with KyroDBClient(target=live_target) as client:
        client.wait_for_ready(timeout_s=5)

        ack = client.insert(
            doc_id=101,
            embedding=[1.0, 0.0, 0.0, 0.0],
            metadata={"tenant": "acme", "status": "active"},
            namespace="default",
        )
        assert ack.success is True

        query = client.query(doc_id=101, include_embedding=True, namespace="default")
        assert query.found is True
        assert query.doc_id == 101
        assert query.embedding is not None
        assert query.metadata["tenant"] == "acme"

        search = client.search(
            query_embedding=[1.0, 0.0, 0.0, 0.0],
            k=5,
            namespace="default",
            filter=exact("tenant", "acme"),
        )
        ids = [hit.doc_id for hit in search.results]
        assert 101 in ids

        update = client.update_metadata(
            doc_id=101,
            metadata={"tenant": "acme", "status": "archived"},
            merge=False,
            namespace="default",
        )
        assert update.success is True

        delete = client.batch_delete_ids(doc_ids=[101], namespace="default")
        assert delete.success is True
        assert delete.deleted_count >= 1

        post = client.query(doc_id=101, namespace="default")
        assert post.found is False


@pytest.mark.integration
@pytest.mark.asyncio
async def test_async_insert_query_search_with_live_server(live_target: str) -> None:
    async with AsyncKyroDBClient(target=live_target) as client:
        await client.wait_for_ready(timeout_s=5)

        inserted = False
        try:
            ack = await client.insert(
                doc_id=202,
                embedding=[0.0, 1.0, 0.0, 0.0],
                metadata={"tenant": "beta"},
                namespace="default",
            )
            assert ack.success is True
            inserted = True

            query = await client.query(doc_id=202, include_embedding=True, namespace="default")
            assert query.found is True
            assert query.embedding is not None

            search = await client.search(
                query_embedding=[0.0, 1.0, 0.0, 0.0],
                k=3,
                namespace="default",
                filter=exact("tenant", "beta"),
            )
            ids = [hit.doc_id for hit in search.results]
            assert 202 in ids
        finally:
            if inserted:
                delete = await client.delete(doc_id=202, namespace="default")
                assert delete.success is True
                assert delete.existed is True
