from abc import ABC
from pathlib import Path
from typing import Optional

from ytdl_sub.config.config_validator import ConfigOptions
from ytdl_sub.config.overrides import Overrides
from ytdl_sub.config.plugin.preset_plugins import PresetPlugins
from ytdl_sub.config.preset import Preset
from ytdl_sub.config.preset_options import OutputOptions
from ytdl_sub.config.preset_options import YTDLOptions
from ytdl_sub.config.validators.variable_validation import VariableValidation
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
from ytdl_sub.entries.variables.override_variables import SubscriptionVariables
from ytdl_sub.utils.exceptions import SubscriptionPermissionError
from ytdl_sub.utils.file_handler import FileHandler
from ytdl_sub.utils.file_handler import FileHandlerTransactionLog
from ytdl_sub.utils.logger import Logger
from ytdl_sub.utils.yaml import dump_yaml
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive

logger = Logger.get("subscription")


def _initialize_download_archive(
    output_options: OutputOptions,
    overrides: Overrides,
    working_directory: str,
    output_directory: str,
) -> EnhancedDownloadArchive:
    migrated_file_name: Optional[str] = None
    if migrated_file_name_option := output_options.migrated_download_archive_name:
        migrated_file_name = overrides.apply_formatter(migrated_file_name_option)

    return EnhancedDownloadArchive(
        file_name=overrides.apply_formatter(output_options.download_archive_name),
        working_directory=working_directory,
        output_directory=output_directory,
        migrated_file_name=migrated_file_name,
    ).reinitialize(dry_run=True)


class BaseSubscription(ABC):
    """
    Subscription classes are the 'controllers' that perform...

    -  Downloading via ytdlp
    -  Adding metadata
    -  Placing files in the output directory

    while configuring each step with provided configs. Child classes are expected to
    provide SourceValidator (SourceT), which defines the source and its configurable options.
    In addition, they should provide in the init an Entry type (EntryT), which is the entry that
    will be returned after downloading.
    """

    def __init__(
        self,
        name: str,
        config_options: ConfigOptions,
        preset_options: Preset,
    ):
        """
        Parameters
        ----------
        name: str
            Name of the subscription
        config_options: ConfigOptions
        preset_options: Preset
        """
        self.name = name
        self._config_options = config_options
        self._preset_options = preset_options

        # Add overrides pre-archive
        self.overrides.add(
            {
                SubscriptionVariables.subscription_name(): self.name,
            }
        )

        # Validate after adding the subscription name
        self._validated_dict = VariableValidation(
            overrides=self.overrides,
            downloader_options=self.downloader_options,
            output_options=self.output_options,
            plugins=self.plugins,
        ).ensure_proper_usage()

        self._enhanced_download_archive: Optional[EnhancedDownloadArchive] = (
            _initialize_download_archive(
                output_options=self.output_options,
                overrides=self.overrides,
                working_directory=self.working_directory,
                output_directory=self.output_directory,
            )
        )

        # Add post-archive variables
        self.overrides.add(
            {
                SubscriptionVariables.subscription_has_download_archive(): (
                    f"{{%bool({self.download_archive.num_entries > 0})}}"
                ),
            }
        )

        self._exception: Optional[Exception] = None

        if not FileHandler.is_path_writable(self.output_directory):
            raise SubscriptionPermissionError(
                "ytdl-sub does not have write permissions to the output directory: "
                f"{self.output_directory}"
            )

    @property
    def download_archive(self) -> EnhancedDownloadArchive:
        """
        Returns
        -------
        Initialized download archive
        """
        assert self._enhanced_download_archive is not None
        return self._enhanced_download_archive

    @property
    def downloader_options(self) -> MultiUrlValidator:
        """
        Returns
        -------
        The download options for this subscription's downloader
        """
        return self._preset_options.downloader_options

    @property
    def plugins(self) -> PresetPlugins:
        """
        Returns
        -------
        List of tuples containing (plugin class, plugin options)
        """
        return self._preset_options.plugins

    @property
    def ytdl_options(self) -> YTDLOptions:
        """
        Returns
        -------
        YTDL options for this subscription
        """
        return self._preset_options.ytdl_options

    @property
    def output_options(self) -> OutputOptions:
        """
        Returns
        -------
        The output options defined for this subscription
        """
        return self._preset_options.output_options

    @property
    def overrides(self) -> Overrides:
        """
        Returns
        -------
        The overrides defined for this subscription
        """
        return self._preset_options.overrides

    @property
    def working_directory(self) -> str:
        """
        Returns
        -------
        The directory that the downloader saves files to
        """
        return str(Path(self._config_options.working_directory) / Path(self.name))

    @property
    def output_directory(self) -> str:
        """
        Returns
        -------
        The formatted output directory
        """
        return self.overrides.apply_formatter(formatter=self.output_options.output_directory)

    @property
    def maintain_download_archive(self) -> bool:
        """
        Returns
        -------
        Whether to maintain a download archive
        """
        return self.output_options.maintain_download_archive

    @property
    def num_entries_added(self) -> int:
        """
        Returns
        -------
        Number of entries added
        """
        return self.download_archive.num_entries_added

    @property
    def num_entries_modified(self) -> int:
        """
        Returns
        -------
        Number of entries modified
        """
        return self.download_archive.num_entries_modified

    @property
    def num_entries_removed(self) -> int:
        """
        Returns
        -------
        Number of entries removed
        """
        return self.download_archive.num_entries_removed

    @property
    def num_entries(self) -> int:
        """
        Returns
        -------
        The number of entries
        """
        return self.download_archive.num_entries

    @property
    def transaction_log(self) -> FileHandlerTransactionLog:
        """
        Returns
        -------
        Transaction log from the subscription
        """
        return self.download_archive.get_file_handler_transaction_log()

    @property
    def exception(self) -> Optional[Exception]:
        """
        Returns
        -------
        An exception if one occurred while processing the subscription
        """
        return self._exception

    def as_yaml(self) -> str:
        """
        Returns
        -------
        Subscription in yaml format
        """
        return self._preset_options.yaml

    def resolved_yaml(self) -> str:
        """
        Returns
        -------
        Human-readable, condensed YAML definition of the subscription.
        """
        return dump_yaml(self._validated_dict)
