"""Configuration for connecting to a Kafka-compatible broker."""

from __future__ import annotations

import os
from dataclasses import dataclass, field


@dataclass
class Config:
    """Holds all configuration for connecting to a Kafka-compatible broker.

    Environment variables (loaded via ``new_config_from_env``):

        NEONLINK_BROKERS                  comma-separated broker addresses
        NEONLINK_TLS_ENABLED              "true" / "false"
        NEONLINK_TLS_CA_FILE              path to CA certificate
        NEONLINK_TLS_CERT_FILE            path to client certificate
        NEONLINK_TLS_KEY_FILE             path to client private key
        NEONLINK_SASL_MECHANISM           PLAIN, SCRAM-SHA-256, SCRAM-SHA-512
        NEONLINK_SASL_USERNAME            SASL username
        NEONLINK_SASL_PASSWORD            SASL password
        NEONLINK_CONSUMER_GROUP           consumer group name
        NEONLINK_MAX_POLL_RECORDS         max records per poll
        NEONLINK_SESSION_TIMEOUT_SEC      session timeout in seconds
        NEONLINK_MAX_POLL_INTERVAL_SEC    max poll interval in seconds
        NEONLINK_AUTO_OFFSET_RESET        "earliest" or "latest"
        NEONLINK_REQUIRED_ACKS            "all" | "1" | "0"
        NEONLINK_TRANSACTIONAL_ID         transactional ID for atomic multi-topic writes
        NEONLINK_CB_MAX_FAILURES          circuit breaker max failures
        NEONLINK_CB_RESET_TIMEOUT_SEC     circuit breaker reset timeout seconds
        NEONLINK_RETRY_MAX_ATTEMPTS       max retry attempts
        NEONLINK_RETRY_INITIAL_WAIT_MS    retry initial wait in milliseconds
        NEONLINK_RETRY_MAX_WAIT_MS        retry max wait in milliseconds
        NEONLINK_DLQ_TOPIC                DLQ topic name
        NEONLINK_POISON_THRESHOLD         poison pill threshold
        NEONLINK_CLIENT_ID                client ID for broker connections
        NEONLINK_LINGER_MS                producer linger in milliseconds
        NEONLINK_BATCH_SIZE               producer batch size in bytes
        NEONLINK_MAX_MESSAGE_BYTES        max single message size in bytes
        NEONLINK_MAX_BUFFERED_RECORDS     in-memory buffered records limit
        NEONLINK_QUEUED_MAX_MESSAGES_KBYTES librdkafka queued buffer limit (KB)
    """

    brokers: list[str] = field(default_factory=lambda: ["localhost:9092"])

    # TLS
    tls_enabled: bool = False
    tls_ca_file: str = ""
    tls_cert_file: str = ""
    tls_key_file: str = ""

    # SASL
    sasl_mechanism: str = ""
    sasl_username: str = ""
    sasl_password: str = ""

    # Consumer
    consumer_group: str = ""
    max_poll_records: int = 10
    session_timeout_sec: int = 30
    max_poll_interval_sec: int = 300
    auto_offset_reset: str = "earliest"

    # Producer
    required_acks: str = "all"  # "all", "1", "0"
    transactional_id: str = ""  # enables Kafka transactions for atomic multi-topic writes

    # Circuit breaker
    cb_max_failures: int = 5
    cb_reset_timeout_sec: int = 30

    # Retry
    retry_max_attempts: int = 3
    retry_initial_wait_ms: int = 500
    retry_max_wait_ms: int = 30000

    # DLQ
    dlq_topic: str = "errors-dlq"
    poison_pill_threshold: int = 5

    # Producer tuning
    linger_ms: int = 5
    batch_size: int = 16384
    max_message_bytes: int = 1048576  # 1 MiB

    # Backpressure
    max_buffered_records: int = 500
    queued_max_messages_kbytes: int = 65536  # 64 MB

    # Metrics (optional). Defaults to NoopMetrics if not set.
    metrics: object = None  # type: Metrics

    # Operational
    client_id: str = ""

    def to_confluent_config(self) -> dict[str, str]:
        """Build a confluent-kafka compatible config dict for the producer or consumer."""
        conf: dict[str, str] = {
            "bootstrap.servers": ",".join(self.brokers),
        }

        if self.client_id:
            conf["client.id"] = self.client_id

        if self.tls_enabled:
            conf["security.protocol"] = "SSL"
            if self.tls_ca_file:
                conf["ssl.ca.location"] = self.tls_ca_file
            if self.tls_cert_file:
                conf["ssl.certificate.location"] = self.tls_cert_file
            if self.tls_key_file:
                conf["ssl.key.location"] = self.tls_key_file

        if self.sasl_mechanism:
            protocol = "SASL_SSL" if self.tls_enabled else "SASL_PLAINTEXT"
            conf["security.protocol"] = protocol
            conf["sasl.mechanism"] = self.sasl_mechanism
            conf["sasl.username"] = self.sasl_username
            conf["sasl.password"] = self.sasl_password

        return conf

    def to_producer_config(self) -> dict[str, str]:
        """Build confluent-kafka producer config."""
        conf = self.to_confluent_config()
        conf["acks"] = self.required_acks
        conf["enable.idempotence"] = "true"
        conf["retries"] = str(self.retry_max_attempts)
        conf["retry.backoff.ms"] = str(self.retry_initial_wait_ms)
        conf["retry.backoff.max.ms"] = str(self.retry_max_wait_ms)
        conf["delivery.timeout.ms"] = "30000"
        conf["linger.ms"] = str(self.linger_ms)
        conf["batch.size"] = str(self.batch_size)
        conf["message.max.bytes"] = str(self.max_message_bytes)
        return conf

    def to_transactional_config(self) -> dict[str, str]:
        """Build confluent-kafka transactional producer config.

        Forces acks=all as required by the Kafka transaction protocol.
        """
        if not self.transactional_id:
            raise ValueError("neonlink: transactional_id is required for transactional producer")
        conf = self.to_confluent_config()
        conf["acks"] = "all"
        conf["transactional.id"] = self.transactional_id
        conf["retries"] = str(self.retry_max_attempts)
        conf["retry.backoff.ms"] = str(self.retry_initial_wait_ms)
        conf["retry.backoff.max.ms"] = str(self.retry_max_wait_ms)
        conf["delivery.timeout.ms"] = "30000"
        conf["enable.idempotence"] = "true"
        conf["linger.ms"] = str(self.linger_ms)
        conf["batch.size"] = str(self.batch_size)
        conf["message.max.bytes"] = str(self.max_message_bytes)
        return conf

    def to_consumer_config(self) -> dict[str, str]:
        """Build confluent-kafka consumer config."""
        conf = self.to_confluent_config()
        conf["group.id"] = self.consumer_group
        conf["auto.offset.reset"] = self.auto_offset_reset
        conf["enable.auto.commit"] = "false"
        conf["session.timeout.ms"] = str(self.session_timeout_sec * 1000)
        conf["max.poll.interval.ms"] = str(self.max_poll_interval_sec * 1000)
        conf["partition.assignment.strategy"] = "cooperative-sticky"
        if self.queued_max_messages_kbytes > 0:
            conf["queued.max.messages.kbytes"] = str(self.queued_max_messages_kbytes)
        return conf

    def validate(self) -> None:
        """Raise ValueError if configuration is invalid."""
        if not self.brokers:
            raise ValueError("neonlink: at least one broker address required")
        for b in self.brokers:
            if not b:
                raise ValueError("neonlink: broker address cannot be empty")
        if self.tls_enabled and not self.tls_ca_file and not self.tls_cert_file:
            raise ValueError("neonlink: TLS enabled but no CA or cert file provided")
        if self.sasl_mechanism:
            if self.sasl_mechanism not in ("PLAIN", "SCRAM-SHA-256", "SCRAM-SHA-512"):
                raise ValueError(f"neonlink: unsupported SASL mechanism: {self.sasl_mechanism}")
            if not self.sasl_username or not self.sasl_password:
                raise ValueError("neonlink: SASL mechanism set but username/password missing")
        if self.poison_pill_threshold < 1:
            raise ValueError("neonlink: poison pill threshold must be >= 1")
        if self.required_acks != "all":
            raise ValueError("neonlink: required_acks must be 'all' for idempotent producer")


def new_config_from_env() -> Config:
    """Load configuration from environment variables with sensible defaults."""
    brokers_str = _env("NEONLINK_BROKERS", "localhost:9092")
    brokers = [b.strip() for b in brokers_str.split(",") if b.strip()]

    return Config(
        brokers=brokers,
        tls_enabled=_env_bool("NEONLINK_TLS_ENABLED", False),
        tls_ca_file=_env("NEONLINK_TLS_CA_FILE", ""),
        tls_cert_file=_env("NEONLINK_TLS_CERT_FILE", ""),
        tls_key_file=_env("NEONLINK_TLS_KEY_FILE", ""),
        sasl_mechanism=_env("NEONLINK_SASL_MECHANISM", ""),
        sasl_username=_env("NEONLINK_SASL_USERNAME", ""),
        sasl_password=_env("NEONLINK_SASL_PASSWORD", ""),
        consumer_group=_env("NEONLINK_CONSUMER_GROUP", ""),
        max_poll_records=_env_int("NEONLINK_MAX_POLL_RECORDS", 10),
        session_timeout_sec=_env_int("NEONLINK_SESSION_TIMEOUT_SEC", 30),
        max_poll_interval_sec=_env_int("NEONLINK_MAX_POLL_INTERVAL_SEC", 300),
        auto_offset_reset=_env("NEONLINK_AUTO_OFFSET_RESET", "earliest"),
        required_acks=_env("NEONLINK_REQUIRED_ACKS", "all"),
        transactional_id=_env("NEONLINK_TRANSACTIONAL_ID", ""),
        cb_max_failures=_env_int("NEONLINK_CB_MAX_FAILURES", 5),
        cb_reset_timeout_sec=_env_int("NEONLINK_CB_RESET_TIMEOUT_SEC", 30),
        retry_max_attempts=_env_int("NEONLINK_RETRY_MAX_ATTEMPTS", 3),
        retry_initial_wait_ms=_env_int("NEONLINK_RETRY_INITIAL_WAIT_MS", 500),
        retry_max_wait_ms=_env_int("NEONLINK_RETRY_MAX_WAIT_MS", 30000),
        dlq_topic=_env("NEONLINK_DLQ_TOPIC", "errors-dlq"),
        poison_pill_threshold=_env_int("NEONLINK_POISON_THRESHOLD", 5),
        client_id=_env("NEONLINK_CLIENT_ID", ""),
        linger_ms=_env_int("NEONLINK_LINGER_MS", 5),
        batch_size=_env_int("NEONLINK_BATCH_SIZE", 16384),
        max_message_bytes=_env_int("NEONLINK_MAX_MESSAGE_BYTES", 1048576),
        max_buffered_records=_env_int("NEONLINK_MAX_BUFFERED_RECORDS", 500),
        queued_max_messages_kbytes=_env_int("NEONLINK_QUEUED_MAX_MESSAGES_KBYTES", 65536),
    )


def _env(key: str, default: str) -> str:
    """Read string env var by key."""
    value = os.environ.get(key)
    if value:
        return value
    return default


def _env_int(key: str, default: int) -> int:
    """Read integer env var by key."""
    raw = os.environ.get(key)
    if not raw:
        return default
    try:
        return int(raw)
    except ValueError:
        return default


def _env_bool(key: str, default: bool) -> bool:
    """Read boolean env var by key."""
    raw = os.environ.get(key)
    if not raw:
        return default
    normalized = raw.lower()
    if normalized in {"1", "true", "t", "yes", "y", "on"}:
        return True
    if normalized in {"0", "false", "f", "no", "n", "off"}:
        return False
    return default
