import copy
import json
import logging
from collections import defaultdict
from pathlib import Path

import rich
from jinja2 import FileSystemLoader, PrefixLoader
from rich.progress import Progress

from .collection import Collection
from .data_object import DataObject
from .engine import engine
from .page import Page
from .plugins import PluginManager, handle_plugin_registration
from .site_map import SiteMap
from .themes import Theme, ThemeManager

try:
    # Get the RE version for display. If it's not set it means we're working locally.
    from render_engine.__version__ import __version__ as re_version
except ImportError:
    re_version = "development"


class Site:
    """
    The site stores your pages and collections to be rendered.

    Attributes:
        site_vars (dict): A dictionary containing site-wide variables and their values.
        plugin_settings (dict): A dictionary containing plugin settings.
        render_html_site_map (bool): Whether to render the generated site map as an HTML page.
        render_xml_site_map (bool): Whether to render the generated site map as XML.

    Methods:
        update_site_vars(**kwargs): Updates the site-wide variables with the given key-value pairs.
        register_plugins(*plugins, **plugin_settings): Registers the specified plugins with the site.
        register_theme(theme): Registers a theme with the site.
        register_themes(*themes): Registers multiple themes with the site.
        update_theme_settings(**settings): Updates the theme settings with the given key-value pairs.
        collection(Collection): Adds a collection to the site's route list.
        page(Page): Adds a page to the site's route list.
        load_themes(): Loads the themes registered with the site.
        render(): Renders all pages and collections added to the site.

    Properties:
        output_path: The output path where the rendered files will be saved.
        static_paths: The paths to static files used in the site.
        template_path: The path to the template files used for rendering.
    """

    site_vars: dict = {
        "SITE_TITLE": "Untitled Site",
        "SITE_URL": "http://localhost:8000/",
        "DATETIME_FORMAT": "%d %b %Y %H:%M %Z",
        "head": set(),
        "theme": {},
    }
    _output_path: str | Path = "output"
    _template_path: str | Path = "templates"
    _static_paths: set = {"static"}
    plugin_settings: dict = {"plugins": defaultdict(dict)}
    render_html_site_map: bool = False
    render_xml_site_map: bool = False

    def __init__(
        self,
    ) -> None:
        self.plugin_manager: PluginManager = PluginManager()
        self.theme_manager = ThemeManager(
            engine=engine,
            output_path=self._output_path,
            static_paths=self._static_paths,
        )
        self.route_list: dict = {}
        self.site_settings: dict = {}
        self.subcollections: dict[str, list] = {"pages": []}
        self.theme_manager.engine.globals.update(self.site_vars)
        self.theme_manager.add_loader(0, FileSystemLoader(self._template_path))
        self._site_map = SiteMap()

    @property
    def output_path(self) -> Path | str:
        return self.theme_manager.output_path

    @output_path.setter
    def output_path(self, output_path: Path | str) -> None:
        self.theme_manager.output_path = output_path

    @property
    def static_paths(self) -> set:
        return self.theme_manager.static_paths

    @static_paths.setter
    def static_paths(self, static_paths: set) -> None:
        self.theme_manager.static_paths = static_paths

    @property
    def site_map(self) -> SiteMap | None:
        return self._site_map

    def update_site_vars(self, **kwargs) -> None:
        self.site_vars.update(**kwargs)
        self.theme_manager.engine.globals.update(self.site_vars)

    def register_plugins(self, *plugins, **plugin_settings) -> None:
        """
        Register plugins for the site

        :param plugins: List of plugins to register
        :param plugin_settings: KW arguments where the key is the plugin name and the value is a dictionary of settings.
        """
        handle_plugin_registration(
            self.plugin_manager,
            [(plugin, plugin_settings.get(plugin.__name__, dict())) for plugin in plugins],
            self.plugin_settings,
        )

    def register_theme(self, theme: Theme) -> None:
        """Overrides the ThemeManager register_theme method to add plugins to the site"""
        self.theme_manager.register_theme(theme)

        if theme.plugins:
            self.register_plugins(*theme.plugins)

    def register_themes(self, *themes: Theme) -> None:
        """
        Register multiple themes.

        Args:
            *themes: Theme objects to register
        """
        for theme in themes:
            self.register_theme(theme)

    def update_theme_settings(self, **settings) -> None:
        for key, value in settings.items():
            self.site_vars["theme"].update({key: value})

    def collection(self, Collection: type[Collection]) -> Collection:
        """
        Add the collection to the route list to be rendered later.

        This is the primary way to add a collection to the site and
        can either be called on an uninstantiated class or on the class definition as a decorator.

        In most cases. You should use the decorator method.

        ```python
        from render_engine import Site, Collection

        site = Site()

        @site.collection # works
        class Pages(Collection):
            pass


        class Posts(Collection):
            pass

        site.collection(Posts) # also works
        ```
        """
        _Collection = Collection()
        _Collection.plugin_manager = copy.deepcopy(self.plugin_manager)
        self.register_themes(*getattr(_Collection, "required_themes", []))

        if plugins := getattr(_Collection, "plugins", []):
            handle_plugin_registration(
                _Collection.plugin_manager,
                plugins,
                getattr(_Collection, "plugin_settings", dict()),
            )

        for plugin in getattr(_Collection, "ignore_plugins", []):
            _Collection.plugin_manager.unregister_plugin(plugin)

        self.route_list[_Collection._slug] = _Collection
        return _Collection

    def page(self, _page: type[Page]) -> Page:
        """
        Add the page to the route list to be rendered later.
        Also remaps `title` in case the user wants to use it in the template rendering.

        This is the primary way to add a page to the site and can either be called
        on an uninstantiated class or on the class definition as a decorator.

        In most cases. You should use the decorator method.

        ```python

        from render_engine import Site, Page

        site = Site()

        @site.page # works
        class Home(Page):
            pass

        class About(Page):
            pass

        site.page(About) # also works
        ```
        """
        page = _page()
        page.title = page._title  # Expose _title to the user through `title`

        # copy the plugin manager, removing any plugins that the page has ignored
        page.plugin_manager = copy.deepcopy(self.plugin_manager)

        if plugins := getattr(page, "plugins", []):
            handle_plugin_registration(page.plugin_manager, plugins, getattr(page, "plugin_settings", dict()))

        for plugin in getattr(page, "ignore_plugins", []):
            page.plugin_manager.unregister_plugin(plugin)

        self.route_list[getattr(page, page._reference)] = page
        return page

    def data_object(self, _data_object: type[DataObject]) -> DataObject:
        """
        Add the data object to the route list to be rendered later.

        This is the primary way to add a data object to the site and can either be called
        on an uninstantiated class or on the class definition as a decorator.

        In most cases. You should use the decorator method.

        ```python

        from render_engine import Site, DataObject

        site = Site()

        data_object = {'key': 'value'}

        @site.data_object  # works
        class MyJson(DataObject):
            data_object = data_object

        class MyJson(DataObject):
            data_object = data_object

        site.page(MyJson) # also works
        ```
        """
        # Since adding a function as a raw attribute on a class will make it a method we need to actually handle
        # it with `staticmethod`. Rather than asking users to do that, we will extract whatever might be set as the
        # serializer function and use the `__init__` for the `DataObject` to set it so that it is properly handled.
        serializer = getattr(_data_object, "serializer", None) or json.dumps
        data_object = _data_object(serializer)

        data_object.plugin_manager = copy.deepcopy(self.plugin_manager)
        if plugins := getattr(data_object, "plugins", []):
            handle_plugin_registration(
                data_object.plugin_manager, plugins, getattr(data_object, "plugin_settings", dict())
            )

        for plugin in getattr(data_object, "ignore_plugins", []):
            data_object.plugin_manager.unregister_plugin(plugin)

        self.route_list[data_object.path_name] = data_object
        return data_object

    def load_themes(self) -> None:
        """
        function for registering the themes with the theme_manager.
        Used prior to rendering and cli-tasks
        """
        # load themes in the ChoiceLoader/FileLoader
        for theme_prefix, theme_loader in self.theme_manager.prefix.items():
            logging.info(f"loading theme: {theme_prefix}")
            self.theme_manager.add_loader(-1, theme_loader)
        # load themes in the PrefixLoader
        self.theme_manager.add_loader(-1, PrefixLoader(self.theme_manager.prefix))

    @property
    def template_path(self) -> str:
        return self.theme_manager.template_path

    @template_path.setter
    def template_path(self, template_path: str) -> None:
        self.theme_manager.add_loader(0, FileSystemLoader(template_path))

    def render(self, site_url: str | None = None) -> None:
        """
        Render all pages and collections.

        These are pages and collections that have been added to the site using
        the [`Site.page`][src.render_engine.Site.page]
        and [`Site.collection`][src.render_engine.Site.collection] methods.

        Render should be called after all pages and collections have been added to the site.

        You can choose to call it manually in your file or
        use the CLI command [`render-engine build`][src.render_engine.cli.build]

        :param site_url: Alternate URL for the site to use in the site map
        """
        rich.print(
            f"[green]Building {repr(self.site_vars.get('SITE_TITLE', 'your site'))} "
            f"with Render Engine version {re_version}"
        )
        with Progress() as progress:
            site_url = site_url if site_url is not None else self.site_vars.get("SITE_URL", "")
            task_site_map = progress.add_task(f"Updating site map. {site_url=}", total=1)

            # self._site_map will be initialized with an empty route list and the site URL pointing
            # to https://localhost:8000/ This task will update to the correct site URL and with the route list
            # as it will be rendered.
            self._site_map.site_url = site_url
            self._site_map.update(self.route_list)

            if self.render_html_site_map:

                @self.page
                class SiteMapPage(Page):
                    title = f"{self.site_vars.get('SITE_TITLE', '')} Site Map"
                    path_name = "site_map.html"
                    content = self._site_map.html
                    template = "page.html"

            if self.render_xml_site_map:

                @self.page
                class SiteMapXml(Page):
                    path_name = "site_map.xml"
                    template = "sitemap.xml"

            progress.update(task_site_map, advance=1)

            pre_build_task = progress.add_task("Loading Pre-Build Plugins and Themes", total=1)
            self.plugin_manager.hook.pre_build_site(
                site=self,
                settings=self.plugin_manager.plugin_settings,
            )

            self.load_themes()
            self.theme_manager.engine.globals.update(self.site_vars)
            progress.update(pre_build_task, advance=1)
            # Parse Route List
            task_add_route = progress.add_task("[blue]Adding Routes", total=len(self.route_list))

            self.theme_manager._render_static()

            self.theme_manager.engine.globals["site"] = self
            self.theme_manager.engine.globals["routes"] = self.route_list

            for slug, entry in self.route_list.items():
                entry.site = self
                progress.update(task_add_route, description=f"[blue]Adding[gold]Route: [blue]{slug}")
                args = []
                match entry:
                    case Page():
                        progress.update(
                            task_add_route,
                            description=f"[blue]Adding[gold]Route: [blue]{entry._slug}",
                        )
                        args = [self.theme_manager]
                    case Collection():
                        progress.update(
                            task_add_route,
                            description=f"[blue]Adding[gold]Route: [blue]Collection {entry._slug}",
                        )
                        pre_build_collection_task = progress.add_task(
                            "Loading Pre-Build-Collection Plugins",
                            total=1,
                        )
                        entry._run_collection_plugins(
                            hook_type="pre_build_collection",
                            site=self,
                        )
                        progress.update(pre_build_collection_task, advance=1)
                    case DataObject():
                        progress.update(
                            task_add_route,
                            description=f"[blue]Adding[gold]Route: [blue]{entry.filename}",
                        )

                entry.render(*args)
                if isinstance(entry, Collection):
                    post_build_collection_task = progress.add_task(
                        "Loading Post-Build-Collection Plugins",
                        total=1,
                    )
                    entry._run_collection_plugins(
                        hook_type="post_build_collection",
                        site=self,
                    )
                    progress.update(post_build_collection_task, advance=1)
                progress.update(task_add_route, advance=1)

            post_build_task = progress.add_task("Loading Post-Build Plugins", total=1)
            self.plugin_manager.hook.post_build_site(
                site=self,
                settings=self.plugin_manager.plugin_settings,
            )
            progress.update(post_build_task, advance=1)
