"""
Docker container managers for PostgreSQL and Neo4j.

Handles creation, health-checking, and cleanup of test containers.
"""

import logging
import socket
import time
from typing import Any, Dict, Optional

import docker
import psycopg

from .config import TestDatabaseConfig, TestNeo4jConfig

logger = logging.getLogger(__name__)


class DockerPostgreSQLManager:
    """Manages PostgreSQL Docker containers for testing."""

    def __init__(self, config: TestDatabaseConfig):
        self.config = config
        self.docker_client = docker.from_env()
        self.container = None
        self._assigned_port: Optional[int] = None

    # ── Lifecycle ──────────────────────────────────────────────────────

    def create_container(self) -> str:
        """Create and start a PostgreSQL container. Returns container ID."""
        errors = self.config.validate()
        if errors:
            raise ValueError(f"Invalid config: {'; '.join(errors)}")

        self._cleanup_existing_container()

        port = self.config.port if self.config.port else self._find_available_port(15432, 25432)
        self._assigned_port = port

        image = self.config.get_postgres_image()
        logger.info("Creating PostgreSQL container %s (port %d)", self.config.get_container_name(), port)

        self.container = self.docker_client.containers.run(
            image,
            command=self.config.get_postgres_command_args(),
            name=self.config.get_container_name(),
            detach=True,
            auto_remove=False,
            ports={"5432/tcp": port},
            environment={
                "POSTGRES_DB": self.config.get_database_name(),
                "POSTGRES_USER": "test_user",
                "POSTGRES_PASSWORD": "test_password",
            },
            mem_limit=self.config.memory_limit,
        )
        return self.container.id

    def wait_for_ready(self, timeout: int = 30) -> bool:
        """Wait for the database to accept connections."""
        deadline = time.time() + timeout
        backoff = 0.5
        while time.time() < deadline:
            try:
                conn = psycopg.connect(
                    host="127.0.0.1",
                    port=self._assigned_port,
                    dbname=self.config.get_database_name(),
                    user="test_user",
                    password="test_password",
                    connect_timeout=5,
                )
                conn.execute("SELECT 1")
                conn.execute("CREATE EXTENSION IF NOT EXISTS vector")
                conn.close()
                logger.info("PostgreSQL container ready on port %d", self._assigned_port)
                return True
            except Exception:
                time.sleep(backoff)
                backoff = min(backoff * 2, 4)
        return False

    def cleanup(self) -> None:
        """Stop and remove the container."""
        if self.container:
            try:
                self.container.stop(timeout=5)
            except Exception:
                pass
            try:
                self.container.remove(force=True, v=True)
            except Exception:
                pass
            self.container = None
            logger.info("Cleaned up PostgreSQL container")

    # ── Connection info ────────────────────────────────────────────────

    def get_connection_params(self) -> Dict[str, Any]:
        return {
            "host": "127.0.0.1",
            "port": self._assigned_port,
            "dbname": self.config.get_database_name(),
            "user": "test_user",
            "password": "test_password",
            "connect_timeout": self.config.connection_timeout,
        }

    def get_django_database_config(self) -> Dict[str, Any]:
        # The container is launched with POSTGRES_DB=<get_database_name()>, so
        # the test DB already exists. Tell Django to reuse it as the TEST name
        # — otherwise DiscoverRunner prepends another "test_" prefix and tries
        # to CREATE DATABASE, which fails as the test_user lacks superuser.
        db_name = self.config.get_database_name()
        return {
            "ENGINE": "django.db.backends.postgresql",
            "NAME": db_name,
            "USER": "test_user",
            "PASSWORD": "test_password",
            "HOST": "127.0.0.1",
            "PORT": str(self._assigned_port),
            "TEST": {"NAME": db_name},
        }

    # ── Helpers ────────────────────────────────────────────────────────

    @property
    def assigned_port(self) -> Optional[int]:
        return self._assigned_port

    def _cleanup_existing_container(self):
        name = self.config.get_container_name()
        try:
            existing = self.docker_client.containers.get(name)
            existing.stop(timeout=5)
            existing.remove(force=True, v=True)
            logger.info("Removed existing container %s", name)
        except docker.errors.NotFound:
            pass

    def _find_available_port(self, start: int, end: int) -> int:
        for port in range(start, end):
            if self._is_port_available(port):
                return port
        raise RuntimeError(f"No available port in range {start}-{end}")

    @staticmethod
    def _is_port_available(port: int) -> bool:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            return s.connect_ex(("127.0.0.1", port)) != 0


class DockerNeo4jManager:
    """Manages Neo4j Docker containers for testing."""

    def __init__(self, config: TestNeo4jConfig):
        self.config = config
        self.docker_client = docker.from_env()
        self.container = None
        self._assigned_bolt_port: Optional[int] = None
        self._assigned_http_port: Optional[int] = None

    def create_container(self) -> str:
        """Create and start a Neo4j container. Returns container ID."""
        errors = self.config.validate()
        if errors:
            raise ValueError(f"Invalid config: {'; '.join(errors)}")

        self._cleanup_existing_container()

        bolt = self.config.port_bolt if self.config.port_bolt else self._find_available_port(27474, 28474)
        http = self.config.port_http if self.config.port_http else self._find_available_port(27687, 28687)
        self._assigned_bolt_port = bolt
        self._assigned_http_port = http

        image = self.config.get_neo4j_image()
        logger.info("Creating Neo4j container %s (bolt=%d, http=%d)", self.config.get_container_name(), bolt, http)

        self.container = self.docker_client.containers.run(
            image,
            name=self.config.get_container_name(),
            detach=True,
            auto_remove=False,
            ports={
                "7687/tcp": bolt,
                "7474/tcp": http,
            },
            environment=self.config.get_env_vars(),
            mem_limit=self.config.memory_limit,
        )
        return self.container.id

    def wait_for_ready(self, timeout: int = 60) -> bool:
        """Wait for Neo4j to accept bolt connections."""
        deadline = time.time() + timeout
        backoff = 1
        while time.time() < deadline:
            try:
                from neo4j import GraphDatabase

                auth = None if not self.config.auth_enabled else ("neo4j", "testpassword")
                driver = GraphDatabase.driver(
                    f"bolt://127.0.0.1:{self._assigned_bolt_port}",
                    auth=auth,
                )
                driver.verify_connectivity()
                driver.close()
                logger.info("Neo4j container ready on bolt port %d", self._assigned_bolt_port)
                return True
            except Exception:
                time.sleep(backoff)
                backoff = min(backoff * 2, 5)
        return False

    def cleanup(self) -> None:
        """Stop and remove the container."""
        if self.container:
            try:
                self.container.stop(timeout=10)
            except Exception:
                pass
            try:
                self.container.remove(force=True, v=True)
            except Exception:
                pass
            self.container = None
            logger.info("Cleaned up Neo4j container")

    def get_bolt_url(self) -> str:
        auth_part = ""
        if self.config.auth_enabled:
            auth_part = "neo4j:testpassword@"
        return f"bolt://{auth_part}127.0.0.1:{self._assigned_bolt_port}"

    @property
    def assigned_bolt_port(self) -> Optional[int]:
        return self._assigned_bolt_port

    @property
    def assigned_http_port(self) -> Optional[int]:
        return self._assigned_http_port

    def _cleanup_existing_container(self):
        name = self.config.get_container_name()
        try:
            existing = self.docker_client.containers.get(name)
            existing.stop(timeout=10)
            existing.remove(force=True, v=True)
            logger.info("Removed existing container %s", name)
        except docker.errors.NotFound:
            pass

    def _find_available_port(self, start: int, end: int) -> int:
        for port in range(start, end):
            if self._is_port_available(port):
                return port
        raise RuntimeError(f"No available port in range {start}-{end}")

    @staticmethod
    def _is_port_available(port: int) -> bool:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            return s.connect_ex(("127.0.0.1", port)) != 0
