"""Generic class to describe Looker views."""

from __future__ import annotations

from typing import Any, Dict, Iterator, List, Optional, Set, TypedDict

from click import ClickException

OMIT_VIEWS: Set[str] = set()


# TODO: Once we upgrade to Python 3.11 mark just `measures` as non-required, not all keys.
class ViewDict(TypedDict, total=False):
    """Represent a view definition."""

    type: str
    tables: List[Dict[str, str]]
    measures: Dict[str, Dict[str, Any]]


class View(object):
    """A generic Looker View."""

    name: str
    view_type: str
    tables: List[Dict[str, Any]]
    namespace: str

    def __init__(
        self,
        namespace: str,
        name: str,
        view_type: str,
        tables: List[Dict[str, Any]],
        **kwargs,
    ):
        """Create an instance of a view."""
        self.namespace = namespace
        self.tables = tables
        self.name = name
        self.view_type = view_type

    @classmethod
    def from_db_views(
        klass,
        namespace: str,
        is_glean: bool,
        channels: List[Dict[str, str]],
        db_views: dict,
    ) -> Iterator[View]:
        """Get Looker views from app."""
        raise NotImplementedError("Only implemented in subclass.")

    @classmethod
    def from_dict(klass, namespace: str, name: str, _dict: ViewDict) -> View:
        """Get a view from a name and dict definition."""
        raise NotImplementedError("Only implemented in subclass.")

    def get_type(self) -> str:
        """Get the type of this view."""
        return self.view_type

    def as_dict(self) -> dict:
        """Get this view as a dictionary."""
        return {
            "type": self.view_type,
            "tables": self.tables,
        }

    def __str__(self):
        """Stringify."""
        return f"name: {self.name}, type: {self.type}, table: {self.tables}, namespace: {self.namespace}"

    def __eq__(self, other) -> bool:
        """Check for equality with other View."""

        def comparable_dict(d):
            return {tuple(sorted([(k, str(v)) for k, v in t.items()])) for t in d}

        if isinstance(other, View):
            return (
                self.name == other.name
                and self.view_type == other.view_type
                and comparable_dict(self.tables) == comparable_dict(other.tables)
                and self.namespace == other.namespace
            )
        return False

    def get_dimensions(
        self, table, v1_name: Optional[str], dryrun
    ) -> List[Dict[str, Any]]:
        """Get the set of dimensions for this view."""
        raise NotImplementedError("Only implemented in subclass.")

    def to_lookml(self, v1_name: Optional[str], dryrun) -> Dict[str, Any]:
        """
        Generate Lookml for this view.

        View instances can generate more than one Looker view,
        for e.g. nested fields and joins, so this returns
        a list.
        """
        raise NotImplementedError("Only implemented in subclass.")

    def get_client_id(self, dimensions: List[dict], table: str) -> Optional[str]:
        """Return the first field that looks like a client identifier."""
        client_id_fields = self.select_dimension(
            {"client_id", "client_info__client_id", "context_id"},
            dimensions,
            table,
        )
        # Some pings purposely disinclude client_ids, e.g. firefox installer
        return client_id_fields["name"] if client_id_fields else None

    def get_document_id(self, dimensions: List[dict], table: str) -> Optional[str]:
        """Return the first field that looks like a document_id."""
        document_id = self.select_dimension("document_id", dimensions, table)
        return document_id["name"] if document_id else None

    def select_dimension(
        self,
        dimension_names: str | set[str],
        dimensions: List[dict],
        table: str,
    ) -> Optional[dict[str, str]]:
        """
        Return the first field that matches dimension name.

        Throws if the query set is greater than one and more than one item is selected.
        """
        if isinstance(dimension_names, str):
            dimension_names = {dimension_names}
        selected = [d for d in dimensions if d["name"] in dimension_names]
        if selected:
            # there should only be one dimension selected from the set
            # if there are multiple options in the dimention_names set.
            if len(dimension_names) > 1 and len(selected) > 1:
                raise ClickException(
                    f"Duplicate {'/'.join(dimension_names)} dimension in {table!r}"
                )
            return selected[0]
        return None
