from __future__ import annotations

import base64
import json
import os
import uuid
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, get_origin

from pydantic import BaseModel, ConfigDict, Field, SecretStr
from typing_extensions import Self

if TYPE_CHECKING:
    from pydantic.fields import FieldInfo

RECURVE_HOME = Path(os.environ.get("RECURVE_HOME", Path.home() / ".recurve"))
CONFIG_FILE_PATH = RECURVE_HOME / "config.json"


class LogConfig(BaseModel):
    """Log configuration for agent logging system."""

    model_config = ConfigDict(extra="ignore")

    log_dir: Path = Field(default=Path("/var/log/reorc"), description="Directory where log files are stored")
    rotation_size: str = Field(
        default="100 MB",
        description="Maximum size of a single log file before rotation (e.g., '50 MB', '100 MB')",
    )
    retention_size: str = Field(
        default="1 GB",
        description="Maximum total size of all log files to retain (e.g., '1 GB', '500 MB')",
    )
    compression: str = Field(
        default="gz", description="Compression format for rotated log files (e.g., 'gz', 'zip', 'bz2')"
    )
    level: str = Field(default="INFO", description="Minimum log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)")
    enable_file_logging: bool = Field(
        default=True, description="Enable file logging (False for console-only in development)"
    )
    enqueue: bool = Field(default=True, description="Enable asynchronous/non-blocking logging")

    @property
    def log_file_path(self) -> Path:
        """Return the main log file path."""
        return self.log_dir / "agent.log"

    @property
    def retention_count(self) -> int:
        """Calculate the number of files to retain: retention_size / rotation_size."""
        return max(1, self._parse_size(self.retention_size) // self._parse_size(self.rotation_size))

    @staticmethod
    def _parse_size(size_str: str) -> int:
        """Parse size string (e.g., '50 MB') to bytes."""
        size_str = size_str.strip().upper()
        units = {
            "TB": 1024**4,
            "GB": 1024**3,
            "MB": 1024**2,
            "KB": 1024,
            "B": 1,
        }

        # Match from largest to smallest unit to avoid "GB" being matched by "B"
        for unit, multiplier in units.items():
            if size_str.endswith(unit):
                number = float(size_str[: -len(unit)].strip())
                return int(number * multiplier)

        # If no unit specified, assume bytes
        return int(float(size_str))

    def validate_config(self) -> tuple[bool, str]:
        """Validate configuration settings."""
        try:
            # Validate size settings
            if self.retention_count < 1:
                return False, "retention_size must be greater than rotation_size"

            # Validate log level
            valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
            if self.level.upper() not in valid_levels:
                return False, f"Invalid log level: {self.level}. Must be one of {valid_levels}"

            # Validate compression format
            valid_compressions = {"gz", "bz2", "zip", "tar", "tar.gz", "tar.bz2"}
            if self.compression not in valid_compressions:
                return False, f"Invalid compression format: {self.compression}"

            return True, ""
        except Exception as e:
            return False, f"Configuration validation error: {e}"


class AgentConfig(BaseModel):
    model_config = ConfigDict(extra="ignore")
    editable_fields: ClassVar[set[str]] = {
        "token",
        "heartbeat_interval",
        "poll_interval",
        "request_timeout",
        "log_config",
    }

    agent_id: uuid.UUID = Field(default_factory=uuid.uuid4, description="The unique identifier of the agent.")
    server_host: str = Field(..., description="The hostname of the server.")
    tenant_domain: str = Field(..., description="The domain of the tenant.")
    token: SecretStr = Field(..., description="The authentication token.")
    logged_in: bool = Field(False, description="Whether the agent is logged in.")
    heartbeat_interval: int = Field(15, description="The interval in seconds to send heartbeat requests.")
    poll_interval: float = Field(1, description="The interval in seconds to poll for new tasks.")
    request_timeout: int = Field(120, description="The timeout in seconds for HTTP requests.")
    default_local_ip: str | None = Field(
        None,
        description="The default local IP address of the agent. If can not get the local IP address, use this address to send to the server.",
    )
    log_config: LogConfig = Field(default_factory=LogConfig, description="Logging configuration")

    def is_valid(self) -> bool:
        return all((self.token.get_secret_value(), self.server_host, self.tenant_domain))

    @property
    def server_url(self) -> str:
        if self.server_host.startswith("http"):
            return self.server_host
        return f"https://{self.server_host}"

    @property
    def websocket_url(self) -> str:
        if self.server_host.startswith("http"):
            return self.server_host.replace("https://", "wss://").replace("http://", "ws://")
        return f"wss://{self.server_host}"

    def set_auth_token(self, encoded_token: str):
        decoded = base64.urlsafe_b64decode(encoded_token.encode()).split(b"::")
        self.tenant_domain = decoded[0].decode()
        self.server_host = decoded[1].decode()
        self.token = SecretStr(base64.urlsafe_b64encode(decoded[2]).decode())

    def clear_auth_token(self):
        self.token = SecretStr("")

    @classmethod
    def load(cls, filename: Path | str | None = None) -> Self:
        if filename is None:
            filename = CONFIG_FILE_PATH
        filename = Path(filename)
        if not filename.exists():
            cfg = cls(server_host="", tenant_domain="", token="")
            cfg.save(filename)
        with open(filename) as f:
            content = f.read()
            data = json.loads(content)

            # Config migration: add default log_config if not present
            if "log_config" not in data:
                data["log_config"] = LogConfig().model_dump(mode="json")
                # Automatically save updated config
                cfg = cls.model_validate(data)
                cfg.save(filename)
                return cfg

            return cls.model_validate_json(content)

    def save(self, filename: Path | None = None):
        if filename is None:
            filename = CONFIG_FILE_PATH

        filename.parent.mkdir(parents=True, exist_ok=True)
        with open(filename, "w") as f:
            content: dict = self.model_dump(mode="json")
            content["token"] = self.token.get_secret_value()
            f.write(json.dumps(content, indent=2))
            f.write("\n")


def parse_value(key: str, value: str) -> Any:
    field_info: "FieldInfo" = AgentConfig.model_fields[key]
    field_type = get_origin(field_info.annotation) or field_info.annotation
    if field_type is SecretStr:
        return SecretStr(value)
    if field_type is bool:
        return value.lower() in {"true", "yes", "y", "1"}
    return field_type(value)


CONFIG: AgentConfig = AgentConfig.load()
