"""
Phederation settings.
"""

import enum
from typing import ClassVar, override
from urllib.parse import urlparse

from pydantic import BaseModel, Field, SecretStr
from pydantic_core import Url
from pydantic_settings import (
    BaseSettings,
    PydanticBaseSettingsSource,
    SettingsConfigDict,
    YamlConfigSettingsSource,
)

from phederation.utils.base import ObjectId, RateLimit


class StorageProviderType(str, enum.Enum):
    Sqlite = "sqlite"
    Postgresql = "postgres"
    Mongodb = "mongodb"


class DeliveryQueueType(str, enum.Enum):
    """Type of delivery for messages."""

    Immediate = "immediate"
    """Immediately sends the message, does not use temporalio."""

    Queue = "queue"
    """Uses a periodically scheduled internal task to poll the message queue and delivers the message asynchronously."""

    Worker = "worker"
    """Tries to connect to the temporalio workflow and delivers the message asynchronously."""

    Automatic = "auto"
    """Chooses between 'Immediate' (lowest priority), 'Queue' (second priority), and 'Worker' (highest priority) depending on the availability of the queue and temporalio."""


class KeycloakSettings(BaseSettings):
    """Separate settings model for the keycloak connection.

    This is only used in `Authentication` and `Authorization` classes.
    It is not part of the `PhedSettings` that are passed to all main classes.
    """

    keycloak_realm: str = Field(default="phederation", alias="KEYCLOAK_REALM")
    keycloak_client_id: str = Field(default="phederation", alias="KEYCLOAK_CLIENT_ID")
    keycloak_client_secret: SecretStr = Field(default=SecretStr("<client secret not set>"), alias="KEYCLOAK_CLIENT_SECRET")

    model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
        env_file=".env", env_file_encoding="utf-8", extra="allow", arbitrary_types_allowed=True
    )


class DomainSettings(BaseModel):
    """Sets the instance-specific URL and ports, including links within a docker environment."""

    url: str = Field(default="localhost", description="The url this instance can be accessed by from outside.")
    port: int | None = Field(default=None, description="Port this instance can be accessed by from outside. Default None, implies port 80.")
    protocol: str = "https"
    docker_url: str | None = Field(
        default=None, description="The url used internally in a docker environment, to access the API without routing the through external network"
    )
    docker_port: int | None = Field(
        default=None, description="The port used internally in a docker environment, to access the API without routing the through external network"
    )
    admin_port: int | None = Field(default=9082, description="The port opened and used for admin mode. It should not be exposed to the web.")

    keycloak_hostname: str = Field(
        default="http://localhost:8070", description="The address (including schema and port) that keycloak is available at"
    )
    keycloak_docker_hostname: str = Field(
        default="http://keycloak:8070",
        description="The address (including schema and port) that keycloak is available at within a docker environment",
    )
    keycloak_port: int = Field(default=8070, description="The port that keycloak is available at")

    @property
    def base_url(self):
        """Constructs an url with "protocol://url:port", i.e. the external schema and netloc of the instance."""
        return Url.build(scheme=self.protocol, host=self.url, port=self.port).__str__()

    @property
    def admin_url(self):
        """Constructs the url of the admin mode, which is always "http://127.0.0.1:<admin_port>"."""
        return Url.build(scheme="http", host="127.0.0.1", port=self.admin_port).__str__()

    @property
    def netloc(self):
        """Constructs an url without the schema, just "url:port", i.e. the external netloc of the instance."""
        base_parsed = urlparse(self.base_url)
        return base_parsed.netloc

    def is_local(self, id: ObjectId) -> bool:
        """Checks if the given id is from an object local to this instance."""
        return id.startswith(self.base_url)


class RatelimiterSettings(BaseModel):
    # TODO: implement rate limit strategy
    strategy: str = Field(default="SLIDING_WINDOW", description="Rate limiting strategy, not implemented")

    default_policy: RateLimit = Field(default=RateLimit(requests=60, period=60), description="Default policy for a new IP, if no other policy applies")
    policies: dict[str, RateLimit] = Field(default={}, description="Special policies for certain domains")


class DatabaseSettings(BaseModel):
    """Settings for the database connection."""

    name: str = Field(default="phederation", description="The name of the database")
    url: str = Field(
        default=...,
        description="The url string for the database connection. Can contain placeholders like ${MONGO_PASSWORD} that are then filled through .env parameters",
    )
    clear_on_initialize: bool = Field(
        default=False,
        description="If the entire database should be cleared when the instance is (re)started. This is useful for testing, and should be set to False in production.",
    )


class StorageSettings(BaseModel):
    """Configures the storage backend used by the instance."""

    provider: str = StorageProviderType.Mongodb.value
    database: DatabaseSettings = Field(default_factory=lambda: DatabaseSettings(url="sqlite:///instance.db"))
    page_size: int = Field(default=50, description="Number of items per page in paginated collections.")


class SecuritySettings(BaseModel):
    """Configures the security settings of the instance, including key rotation and http signatures."""

    mock_keycloak: bool = Field(default=False, description="If the keycloak connection should be mocked (for testing). Default: False.")
    key_rotation_enabled: bool = Field(
        default=False,
        description="If key rotation should be performed at all. Note that changing keys might ban an instance from certain Mastodon instances, so be careful with enabling key rotation.",
    )
    key_rotation_period: int = Field(default=30 * 24 * 3600, description="Number of seconds until actor keys are rotated.")
    key_rotation_overlap: int = 2
    key_size: int = Field(default=2048, description="Size of each actor key, in bytes.")
    signature_ttl: int = Field(default=300, description="Time-to-live, i.e. number of seconds a signature of a header is considered to be valid.")
    max_payload_size: int = 5_000_000  # 5MB
    allowed_algorithms: list[str] = Field(default_factory=lambda: ["rsa-sha256", "hs2019"])
    verify_ssl: bool = True  # TODO: implement ssl verification
    require_body_digest: bool = Field(
        default=True, description="If http signature verification also requires that the request body digest is included."
    )
    create_actor_from_token: bool = Field(
        default=True, description="If a local actor should be created automatically from the UserInfo if somebody logs into keycloak successfully"
    )


class FederationSettings(BaseModel):
    """Configures the instance and how it interacts with the federated network."""

    instance_name: str = "phederation"
    logging_prefix: str = Field(
        default="phederation", description="Will be added before every logger name. This can be used to distinguish admin and main instance logs."
    )
    auto_accept_follows_default: bool = Field(
        default=True,
        description="If follows should be accepted by default for actors that are created on this instance. The setting can be switched later, separately for each actor.",
    )
    delivery_timeout: int = Field(default=30, description="Timeout in seconds before a single delivery will fail, and then retry.")
    update_nodeinfo_period: int = Field(default=30 * 60, description="Number of seconds until the instance NodeInfo is updated.")
    delivery_queue_polling_period: int = Field(
        default=30, description="Number of seconds until new messages are polled from the delivery quue to send."
    )
    max_concurrent: int = Field(default=50, description="Maximum number of recipients to send to concurrently")
    retry_delay: int = Field(default=300, description="Delay in seconds until sending an activity is tried again (with exponential backoff).")
    max_retries: int = Field(default=3, description="Number of tries before sending an activity is considered failed.")
    cache_ttl: int = Field(default=60, description="Seconds for caching of actors and objects. No cache is created if this is set to zero.")
    cache_max: int = Field(default=10_000, description="Maximum number of objects in cache. No cache is created if this is set to zero.")
    delivery_queuing: str = Field(
        default=DeliveryQueueType.Automatic.value,
        description="Behavior for queueing of activity sending tasks. Immediate does not use a worker environment and send all messages directly, Worker will use temporalio workflows to queue delivery, and Automatic will choose between the two modes based on avaiability of the worker environment.",
    )
    max_concurrent_resolve: int = Field(default=50, description="Maximum number of ids to resolve concurrently in the resolve endpoint")
    endpoints: list[str] = Field(
        default_factory=lambda: ["sharedInbox", "objects", "media", "collections", "resolve"],
        examples=[["sharedInbox", "objects", "media"], ["media"]],
        description="Endpoints supported by the instance. With the current phederation software, this can be 'sharedInbox', 'objects', 'media', 'collections', 'resolve'.",
    )
    admin_mode: bool = Field(
        default=False,
        description="If True, puts the instance in admin mode. In this mode, no incoming requests are accepted if they do not originate from localhost/127.0.0.1, and additional API routes are added that allow maintenance for the instance.",
    )


class MediaSettings(BaseModel):
    """Configures how the Phederation instance handles media files.

    The settings here also allow to add higher security, including adding DataIntegrityProofs to objects and creating file hashes.
    """

    max_size: int = Field(default=10_000_000, description="Maximum file size for uploads that can be sent to the media endpoint.")
    create_file_hash: bool = Field(
        default=True,
        description="If set to True, will attach a SHA256-hash of file contents to the Document of media files, instead of signing just the 'content' field.",
    )
    proof_object_hash: bool = Field(
        default=False,
        description="If set to True, will attach a DataIntegrityProof object to all 'Document' objects, so that their integrity can be checked remotely. The proof only signs the 'hash' field of the 'Document', not the entire body.",
    )
    require_file_hash: bool = Field(
        default=False,
        description="If set to True, will verify that the SHA256-hash for files sent to the endpoint is correct. Processing fails with an exception if verification fails.",
    )
    require_content_hash: bool = Field(
        default=False,
        description="If set to True, will verify that the SHA256-hash of the object.content shared in client-to-server and server-to-server communication is attached and correct. Processing fails with an exception if verification fails.",
    )
    require_file_proof_in_inbox: bool = Field(
        default=False,
        description="If set to True, will verify the SHA256-hash of media shared in server2server communication using a DataIntegrityProof attached to the object. Processing fails with an exception if there is no DataIntegrityProof attached or if verification fails.",
    )
    allowed_types: list[str] = Field(
        default_factory=lambda: [
            "image/jpeg",
            "image/png",
            "image/gif",
            "video/mp4",
            "audio/mpeg",
            "text/txt",
            "text/json",
            "text/yaml",
            "application/json",
        ],
        description="Mime types of files that this endpoint accepts.",
    )


class StatisticsSettings(BaseModel):
    """Configures basic statistics logging in middleware. Disabled by default."""

    enabled: bool = True
    log_activities_incoming: bool = Field(
        default=True,
        description="If incoming activities from other instances should be logged. The content is not logged, just the type, actor_id, and the remote url",
    )
    log_activities_outgoing: bool = Field(
        default=True,
        description="If outgoing activities to other instances should be logged. The content is not logged, just the type, actor_id, and the remote url",
    )
    log_middleware: bool = Field(
        default=False,
        description="Log all middleware requests. This is very verbose and should only be used for debugging. It also is a security risk as user tokens can be logged, too",
    )
    disable_routes: list[str] = Field(default_factory=lambda: ["/auth", "/keys"])


class WorkerSettings(BaseModel):
    """Configures the interface to [temporalIO](https://temporal.io)."""

    enabled: bool = Field(default=True, description="If an asynchronous (temporalio) worker environment is available and should be used.")
    start_own_worker: bool = Field(
        default=True,
        description="If true, a single temporalio worker is started together with this instance. This is useful if no separate worker processes are started for scaling up, and the instance should handle all requests by itself.",
    )
    encrypt_in_transit: bool = Field(
        default=True,
        description="If enabled, encrypts the messages sent to and from the temporalio backend with a Fernet key. This means only the local instance can read the input and output, not a temporal server.",
    )
    tasks_queue_name: str = Field(default="temporal-workflows-queue", description="Name of the temporal worker task queue")


class PhedSettings(BaseSettings, case_sensitive=False):
    """Main settings class.

    Stores all types of settings, and is passed to all main classes (server, resolver, delivery, etc.) for configuration.
    """

    def __init__(self, yaml_file: str, env_file: str = ".env"):
        self.model_config["yaml_file"] = yaml_file
        self.model_config["env_file"] = env_file
        super().__init__()

    version: str = "0.3.3"
    domain: DomainSettings = Field(default=DomainSettings())
    storage: StorageSettings = Field(default=StorageSettings())
    security: SecuritySettings = Field(default=SecuritySettings())
    federation: FederationSettings = Field(default=FederationSettings())
    rate_limiter: RatelimiterSettings = Field(default=RatelimiterSettings())
    media: MediaSettings = Field(default=MediaSettings())
    statistics: StatisticsSettings = Field(default=StatisticsSettings())
    worker: WorkerSettings = Field(default=WorkerSettings())

    model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
        yaml_file="settings/conf.yaml",
        env_prefix="PHEDERATION_",
        env_file=".env",
        env_file_encoding="utf-8",
        env_nested_delimiter="__",
        yaml_file_encoding="utf-8",
        extra="ignore",
    )

    @classmethod
    @override
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        """Customizes the file sources to load the settings.

        Note that the ORDER of the values returned here determines the priority of values loaded from different files.
        The first item in the list is the highest priority.

        Current order (highest first)
          - YAML file
          - ENV file
          - DotENV settings

        Args:
            settings_cls (type[BaseSettings]): Base settings class, used by YamlConfig.
            init_settings (PydanticBaseSettingsSource): Base class, not used.
            env_settings (PydanticBaseSettingsSource): Load settings from env file.
            dotenv_settings (PydanticBaseSettingsSource): Load settings from dotenv module.
            file_secret_settings (PydanticBaseSettingsSource): Load files from secrets.

        Returns:
            tuple[PydanticBaseSettingsSource, ...]: A list of file settings, in specific order.

        """
        return (
            YamlConfigSettingsSource(settings_cls),
            env_settings,
            dotenv_settings,
            init_settings,
            file_secret_settings,
        )
