from __future__ import annotations

from abc import ABCMeta, abstractmethod
from functools import cached_property
from typing import Sequence, TYPE_CHECKING, final

from more_itertools.recipes import unique_everseen

from sobiraka.utils import MISSING, RelativePath
from ..aggregationpolicy import AggregationPolicy
from ..href import PageHref
from ..issues import Issue
from ..status import ObjectWithStatus

if TYPE_CHECKING:
    from sobiraka.models import Page, PageMeta, Volume


class Source(ObjectWithStatus, metaclass=ABCMeta):
    """
    A source is something that generates pages.
    Sources are conceptually close to the files from which content is being loaded,
    not to the files to which it is being eventually written — for that, see Page.

    Each source is characterized by a unique path to a file or a directory (can be fake),
    regardless of whether it is reflected in the generated pages' Locations.

    A source generates zero or more child sources and zero or more pages.
    This process is called discovery.
    """

    aggregation_policy: AggregationPolicy = AggregationPolicy.WAIT_FOR_CHILDREN

    def __init__(self, volume: Volume, path_in_project: RelativePath, *, parent: Source = None):
        self.volume: Volume = volume
        self.path_in_project: RelativePath = path_in_project
        self.parent: Source | None = parent

        self.base_meta: PageMeta | None = None

        # Fields that are being filled while processing
        self.child_sources: Sequence[Source] = MISSING
        self.pages: Sequence[Page] = MISSING
        self.index_page: Page | None = None
        self.subtree_has_pages: bool = False

        # Fields for collecting bad results
        self.issues: list[Issue] = []
        self.exception: Exception | None = None

    def __repr__(self):
        return f'<{self.__class__.__name__}: {self.path_in_project}>'

    @property
    def path_in_volume(self) -> RelativePath:
        return self.path_in_project.relative_to(self.volume.root_path)

    @final
    @cached_property
    def breadcrumbs(self) -> Sequence[Source]:
        """
        A sequence that includes the current source after all its parents, including the root source.
        This may or may not be similar to the corresponding `Page.breadcrumbs`.
        """
        breadcrumbs: list[Source] = [self]
        while breadcrumbs[0].parent is not None:
            breadcrumbs.insert(0, breadcrumbs[0].parent)
        return tuple(breadcrumbs)

    @abstractmethod
    def href(self, identifier: str = None) -> PageHref:
        """
        Without an argument, return the PageHref for the source's main page.
        With an argument, resolve it to that page's anchor or something similar.
        """

    async def generate_child_sources(self):
        """
        Fill `child_sources` with a sequence of sources that are discovered via the current source.
        The most popular example is generating a list of files and subdirectories for a SourceDirectory.
        Sobiraka will start loading the generated sources immediately after this method returns.

        Sobiraka only calls this method once for each source.

        The default implementation generates an empty sequence of child sources.
        """
        self.child_sources = ()

    async def generate_pages(self):
        """
        Fill `pages` with a sequence of pages that are loaded via the current source.
        The most popular example is generating just one page from a Markdown file.
        Sobiraka will start processing the generated pages immediately after this method returns.

        Sobiraka only calls this method once for each source,
        and it guarantees that it does so after `generate_child_sources()`.

        The default implementation generates an empty sequence of pages.
        """
        self.pages = ()

    @final
    def all_child_sources(self) -> Sequence[Source]:
        """
        Return all sources in a subtree under the current source, excluding the current source.
        """
        sources = []
        for child in self.child_sources:
            sources += child, *child.all_child_sources()
        return sources

    @final
    def all_pages(self) -> Sequence[Page]:
        """
        Return all pages generated by the current source and its subtree.
        """
        pages = list(self.pages)
        for child in self.child_sources:
            pages += child.all_pages()
        return tuple(unique_everseen(pages))

    @final
    def _set_index_page(self, index_page: Page):
        """
        Set the given page as this source's index page
        and arrange the necessary parent-children relations
        """
        self.index_page = index_page
        self.index_page.children = []

        # Use the directory's base meta for the index page's meta
        if self.base_meta:
            index_page.meta = self.base_meta + index_page.meta

        for child in self.child_sources:

            # A subdirectory's index page becomes a child of the index page
            if child.index_page is not None:
                child.index_page.parent = index_page
                index_page.children.append(child.index_page)

            # A normal page becomes a child of the index page
            for page in child.pages:
                if page not in (index_page, *index_page.children):
                    page.parent = index_page
                    index_page.children.append(page)


class IdentifierResolutionError(Exception):
    pass
