import functools
import itertools
from pathlib import Path
from typing import ClassVar, Iterable

import funcy
from rich.console import RenderResult
from rich.text import Text
from textual.binding import Binding, BindingType
from textual.containers import Vertical  # removed unused VerticalScroll (F401)
from textual.reactive import Reactive
from textual.widget import Widget
from textual.widgets import Label, ListItem, ListView

from iolanta.facets.facet import Facet
from iolanta.facets.page_title import PageTitle
from iolanta.facets.textual_default.triple_uri_ref import TripleURIRef
from iolanta.models import NotLiteralNode
from iolanta.namespaces import DATATYPES, RDF

INSTANCE_RENDER_RADIUS = 50


class InstanceItem(ListItem):
    """An item in class instances list."""

    renderable: Reactive[RenderResult | None](None, init=True, layout=True)

    def __init__(
        self,
        node: NotLiteralNode,
        parent_class: NotLiteralNode,
    ):
        """Specify the node, its class, and that we are not rendered yet."""
        self.node = node
        self.parent_class = parent_class
        self.renderable = None
        super().__init__()

    def render(self) -> RenderResult:
        """
        Render this class instance.

        Render either the result or the raw node.

        # FIXME Calculate QName at least. Or CURIE? Or whatsit?
        """
        qname = self.app.iolanta.node_as_qname(self.node)
        return self.renderable or Text(
            f'⏳ {qname}',
            style='#696969',
        )

    def update(self, renderable: RenderResult):
        """
        Assign the render result.

        A separate method is needed for this because we call it from a thread.
        """
        self.renderable = renderable
        self.refresh()


class InstancesList(ListView):   # noqa: WPS214
    """Instances list."""

    BINDINGS: ClassVar[list[BindingType]] = [
        Binding('enter', 'goto', 'Goto'),
        Binding('p', 'provenance', 'Provenan©e'),
    ]

    DEFAULT_CSS = """
    InstancesList {
        height: 1fr;
        layout: vertical;
        overflow-y: auto;
    }
    """

    FIRST_CHUNK_SIZE = 15
    DEFAULT_CHUNK_SIZE = 10

    def __init__(
        self,
        instances: Iterable[NotLiteralNode],
        parent_class: NotLiteralNode,
    ):
        """Specify the instances to render and their class."""
        self.instances = instances
        self.parent_class = parent_class
        super().__init__()

    def render_newly_added_instances(self, instance_items: list[InstanceItem]):
        """
        Render each of the given instance items.

        Must be called in a worker.
        """
        for instance_item in instance_items:
            self.app.call_from_thread(
                instance_item.update,
                self.app.iolanta.render(
                    instance_item.node,
                    as_datatype=DATATYPES.title,
                ),
            )

    def stream_instance_items_chunk(
        self,
        count: int | None = None,
    ) -> Iterable[InstanceItem]:
        """Return a chunk of unique class instances."""
        chunk = itertools.islice(
            self.instances,
            count or self.DEFAULT_CHUNK_SIZE,
        )
        for instance in chunk:  # noqa: WPS526
            yield InstanceItem(
                node=instance,
                parent_class=self.parent_class,
            )

    def action_goto(self):
        """Navigate."""
        self.app.action_goto(self.highlighted_child.node)

    def action_provenance(self):
        """Navigate to provenance for the property value."""
        self.app.action_goto(
            TripleURIRef.from_triple(
                subject=self.highlighted_child.node,
                predicate=RDF.type,
                object_=self.parent_class,
            ),
        )

    def compose(self):
        """Fill in the instances."""
        for instance in self.instances:   # noqa: WPS526
            yield InstanceItem(
                node=instance,
                parent_class=self.parent_class,
            )

    def render_instances(self):
        """Render all instances."""
        for instance_item in self.children:
            self.app.call_from_thread(
                instance_item.update,
                self.app.iolanta.render(
                    instance_item.node,
                    as_datatype=DATATYPES.title,
                ),
            )

    def on_mount(self):
        """Render the first chunk of instances."""
        self.run_worker(
            self.render_instances,
            thread=True,
        )


class Bottom(Label):
    """Label below the instances list."""

    DEFAULT_CSS = """
    Bottom {
        padding-top: 1;
        padding-bottom: 1;
        dock: bottom;
    }
    """


class InstancesBody(Vertical):
    """Container for instances list and accompanying bells and whistles."""

    DEFAULT_CSS = """
    InstancesBody {
        height: auto;
        max-height: 100%;
    }
    """


class Class(Facet[Widget]):
    """Render instances of a class."""

    META = Path(__file__).parent / 'textual-class.yamlld'

    def stream_instances(self) -> Iterable[NotLiteralNode]:
        """
        Query and stream class instances lazily.

        The operation of rendering an instance is not pure: it might cause us
        to retrieve more data and load said data into the graph. That's because
        we do multiple query attempts.

        We have to stop if a subsequent attempt returns no results. That's why
        we can't use `funcy.distinct()` or something similar.
        """
        return set(
            funcy.pluck(
                'instance',
                self.stored_query('instances.sparql', this=self.this),
            ),
        )

    def show(self) -> Widget:
        """Render the instances list."""
        return InstancesBody(
            PageTitle(self.this),
            InstancesList(
                instances=list(self.stream_instances()),
                parent_class=self.this,
            ),
        )
