""" QueryOptions class for PhotosDB.query """

import dataclasses
import datetime
import io
import pathlib
import re
import sys
from dataclasses import asdict, dataclass
from typing import Iterable, List, Optional, Tuple

import bitmath

from ._constants import UUID_PATTERN

__all__ = ["QueryOptions", "query_options_from_kwargs", "IncompatibleQueryOptions"]


class IncompatibleQueryOptions(Exception):
    """Incompatible query options"""

    pass


@dataclass
class QueryOptions:

    """QueryOptions class for PhotosDB.query

    Attributes:
        added_after: search for photos added on or after a given date
        added_before: search for photos added before a given date
        added_in_last: search for photos added in last X datetime.timedelta
        album: list of album names to search for
        burst_photos: include all associated burst photos for photos in query results
        burst: search for burst photos
        cloudasset: search for photos that are managed by iCloud
        deleted_only: search only for deleted photos
        deleted: also include deleted photos
        description: list of descriptions to search for
        duplicate: search for duplicate photos
        edited: search for edited photos
        exif: search for photos with EXIF tags that matches the given data
        external_edit: search for photos edited in external apps
        favorite: search for favorite photos
        folder: list of folder names to search for
        from_date: search for photos taken on or after this date
        from_time: search for photos taken on or after this time of day
        function: list of query functions to evaluate
        has_comment: search for photos with comments
        has_likes: search for shared photos with likes
        has_raw: search for photos with associated raw files
        hdr: search for HDR photos
        hidden: search for hidden photos
        ignore_case: ignore case when searching
        in_album: search for photos in an album
        incloud: search for cloud assets that are synched to iCloud
        is_reference: search for photos stored by reference (that is, they are not managed by Photos)
        keyword: list of keywords to search for
        label: list of labels to search for
        live: search for live photos
        location: search for photos with a location
        max_size: maximum size of photos to search for
        min_size: minimum size of photos to search for
        missing_bursts: for burst photos, also include burst photos that are missing
        missing: search for missing photos
        movies: search for movies
        name: list of names to search for
        no_comment: search for photos with no comments
        no_description: search for photos with no description
        no_likes: search for shared photos with no likes
        no_location: search for photos with no location
        no_keyword: search for photos with no keywords
        no_place: search for photos with no place
        no_title: search for photos with no title
        not_burst: search for non-burst photos
        not_cloudasset: search for photos that are not managed by iCloud
        not_edited: search for photos that have not been edited
        not_favorite: search for non-favorite photos
        not_hdr: search for non-HDR photos
        not_hidden: search for non-hidden photos
        not_in_album: search for photos not in an album
        not_incloud: search for cloud asset photos that are not yet synched to iCloud
        not_live: search for non-live photos
        not_missing: search for non-missing photos
        not_panorama: search for non-panorama photos
        not_portrait: search for non-portrait photos
        not_reference: search for photos not stored by reference (that is, they are managed by Photos)
        not_screenshot: search for non-screenshot photos
        not_selfie: search for non-selfie photos
        not_shared: search for non-shared photos
        not_slow_mo: search for non-slow-mo photos
        not_time_lapse: search for non-time-lapse photos
        panorama: search for panorama photos
        person: list of person names to search for
        photos: search for photos
        place: list of place names to search for
        portrait: search for portrait photos
        query_eval: list of query expressions to evaluate
        regex: list of regular expressions to search for
        screenshot: search for screenshot photos
        selected: search for selected photos
        selfie: search for selfie photos
        shared: search for shared photos
        slow_mo: search for slow-mo photos
        time_lapse: search for time-lapse photos
        title: list of titles to search for
        to_date: search for photos taken before this date
        to_time: search for photos taken before this time of day
        uti: list of UTIs to search for
        uuid: list of uuids to search for
        year: search for photos taken in a given year
        syndicated: search for photos that have been shared via syndication ("Shared with You" album via Messages, etc.)
        not_syndicated: search for photos that have not been shared via syndication ("Shared with You" album via Messages, etc.)
        saved_to_library: search for syndicated photos that have been saved to the Photos library
        not_saved_to_library: search for syndicated photos that have not been saved to the Photos library
        shared_moment: search for photos that have been shared via a shared moment
        not_shared_moment: search for photos that have not been shared via a shared moment
        shared_library: search for photos that are part of a shared iCloud library
        not_shared_library: search for photos that are not part of a shared iCloud library
    """

    added_after: Optional[datetime.datetime] = None
    added_before: Optional[datetime.datetime] = None
    added_in_last: Optional[datetime.timedelta] = None
    album: Optional[Iterable[str]] = None
    burst_photos: Optional[bool] = None
    burst: Optional[bool] = None
    cloudasset: Optional[bool] = None
    deleted_only: Optional[bool] = None
    deleted: Optional[bool] = None
    description: Optional[Iterable[str]] = None
    duplicate: Optional[bool] = None
    edited: Optional[bool] = None
    exif: Optional[Iterable[Tuple[str, str]]] = None
    external_edit: Optional[bool] = None
    favorite: Optional[bool] = None
    folder: Optional[Iterable[str]] = None
    from_date: Optional[datetime.datetime] = None
    from_time: Optional[datetime.time] = None
    function: Optional[List[Tuple[callable, str]]] = None
    has_comment: Optional[bool] = None
    has_likes: Optional[bool] = None
    has_raw: Optional[bool] = None
    hdr: Optional[bool] = None
    hidden: Optional[bool] = None
    ignore_case: Optional[bool] = None
    in_album: Optional[bool] = None
    incloud: Optional[bool] = None
    is_reference: Optional[bool] = None
    keyword: Optional[Iterable[str]] = None
    label: Optional[Iterable[str]] = None
    live: Optional[bool] = None
    location: Optional[bool] = None
    max_size: Optional[bitmath.Byte] = None
    min_size: Optional[bitmath.Byte] = None
    missing_bursts: Optional[bool] = None
    missing: Optional[bool] = None
    movies: Optional[bool] = True
    name: Optional[Iterable[str]] = None
    no_comment: Optional[bool] = None
    no_description: Optional[bool] = None
    no_likes: Optional[bool] = None
    no_location: Optional[bool] = None
    no_keyword: Optional[bool] = None
    no_place: Optional[bool] = None
    no_title: Optional[bool] = None
    not_burst: Optional[bool] = None
    not_cloudasset: Optional[bool] = None
    not_edited: Optional[bool] = None
    not_favorite: Optional[bool] = None
    not_hdr: Optional[bool] = None
    not_hidden: Optional[bool] = None
    not_in_album: Optional[bool] = None
    not_incloud: Optional[bool] = None
    not_live: Optional[bool] = None
    not_missing: Optional[bool] = None
    not_panorama: Optional[bool] = None
    not_portrait: Optional[bool] = None
    not_reference: Optional[bool] = None
    not_screenshot: Optional[bool] = None
    not_selfie: Optional[bool] = None
    not_shared: Optional[bool] = None
    not_slow_mo: Optional[bool] = None
    not_time_lapse: Optional[bool] = None
    panorama: Optional[bool] = None
    person: Optional[Iterable[str]] = None
    photos: Optional[bool] = True
    place: Optional[Iterable[str]] = None
    portrait: Optional[bool] = None
    query_eval: Optional[Iterable[str]] = None
    regex: Optional[Iterable[Tuple[str, str]]] = None
    screenshot: Optional[bool] = None
    selected: Optional[bool] = None
    selfie: Optional[bool] = None
    shared: Optional[bool] = None
    slow_mo: Optional[bool] = None
    time_lapse: Optional[bool] = None
    title: Optional[Iterable[str]] = None
    to_date: Optional[datetime.datetime] = None
    to_time: Optional[datetime.time] = None
    uti: Optional[Iterable[str]] = None
    uuid: Optional[Iterable[str]] = None
    year: Optional[Iterable[int]] = None
    syndicated: Optional[bool] = None
    not_syndicated: Optional[bool] = None
    saved_to_library: Optional[bool] = None
    not_saved_to_library: Optional[bool] = None
    shared_moment: Optional[bool] = None
    not_shared_moment: Optional[bool] = None
    shared_library: Optional[bool] = None
    not_shared_library: Optional[bool] = None

    def asdict(self):
        return asdict(self)


def query_options_from_kwargs(**kwargs) -> QueryOptions:
    """Validate query options and create a QueryOptions instance.
    Note: this will block on stdin if uuid_from_file is set to "-"
    so it is best to call function before creating the PhotosDB instance
    so that the validation of query options can happen before the database
    is loaded.
    """
    # sanity check input args
    nonexclusive = [
        "added_after",
        "added_before",
        "added_in_last",
        "album",
        "duplicate",
        "exif",
        "external_edit",
        "folder",
        "from_date",
        "from_time",
        "has_raw",
        "keyword",
        "label",
        "max_size",
        "min_size",
        "name",
        "person",
        "query_eval",
        "query_function",
        "regex",
        "selected",
        "to_date",
        "to_time",
        "uti",
        "uuid",
        "uuid_from_file",
        "year",
    ]
    exclusive = [
        ("burst", "not_burst"),
        ("cloudasset", "not_cloudasset"),
        ("edited", "not_edited"),
        ("favorite", "not_favorite"),
        ("has_comment", "no_comment"),
        ("has_likes", "no_likes"),
        ("hdr", "not_hdr"),
        ("hidden", "not_hidden"),
        ("in_album", "not_in_album"),
        ("incloud", "not_incloud"),
        ("is_reference", "not_reference"),
        ("keyword", "no_keyword"),
        ("live", "not_live"),
        ("location", "no_location"),
        ("missing", "not_missing"),
        ("only_photos", "only_movies"),
        ("panorama", "not_panorama"),
        ("portrait", "not_portrait"),
        ("screenshot", "not_screenshot"),
        ("selfie", "not_selfie"),
        ("shared", "not_shared"),
        ("slow_mo", "not_slow_mo"),
        ("time_lapse", "not_time_lapse"),
        ("deleted", "not_deleted"),
        ("deleted", "deleted_only"),
        ("deleted_only", "not_deleted"),
        ("syndicated", "not_syndicated"),
        ("saved_to_library", "not_saved_to_library"),
        ("shared_moment", "not_shared_moment"),
        ("shared_library", "not_shared_library"),
    ]

    # TODO: add option to validate requiring at least one query arg
    for arg, not_arg in exclusive:
        if kwargs.get(arg) and kwargs.get(not_arg):
            arg = arg.replace("_", "-")
            not_arg = not_arg.replace("_", "-")
            raise IncompatibleQueryOptions(
                f"Incompatible query options: --{arg} and --{not_arg} are mutually exclusive"
            )

    # some options like title can be specified multiple times
    # check if any of them are specified along with their no_ counterpart
    exclusive_multi_options = ["title", "description", "place", "keyword"]
    for option in exclusive_multi_options:
        if kwargs.get(option) and kwargs.get("no_{option}"):
            raise IncompatibleQueryOptions(
                f"--{option} and --no-{option} are mutually exclusive"
            )

    include_photos = True
    include_movies = True  # default searches for everything
    if kwargs.get("only_movies"):
        include_photos = False
    if kwargs.get("only_photos"):
        include_movies = False

    # load UUIDs if necessary and append to any uuids passed with --uuid
    uuids = list(kwargs.get("uuid", []))  # Click option is a tuple
    if uuid_from_file := kwargs.get("uuid_from_file"):
        uuids.extend(load_uuid_from_file(uuid_from_file))
        uuids = tuple(uuids)

    query_fields = [field.name for field in dataclasses.fields(QueryOptions)]
    query_dict = {field: kwargs.get(field) for field in query_fields}
    query_dict["photos"] = include_photos
    query_dict["movies"] = include_movies
    query_dict["uuid"] = uuids
    query_dict["function"] = kwargs.get("query_function")

    return QueryOptions(**query_dict)


def load_uuid_from_file(filename: str) -> list[str]:
    """
    Load UUIDs from file.
    Does not validate UUIDs but does validate that the UUIDs are in the correct format.
    Format is 1 UUID per line, any line beginning with # is ignored.
    Whitespace is stripped.

    Arguments:
        filename: file name of the file containing UUIDs

    Returns:
        list of UUIDs or empty list of no UUIDs in file

    Raises:
        FileNotFoundError if file does not exist
        ValueError if UUID is not in correct format
    """

    if filename == "-":
        return _load_uuid_from_stream(sys.stdin)

    if not pathlib.Path(filename).is_file():
        raise FileNotFoundError(f"Could not find file {filename}")

    with open(filename, "r") as f:
        return _load_uuid_from_stream(f)


def _load_uuid_from_stream(stream: io.IOBase) -> list[str]:
    """
    Load UUIDs from stream.
    Does not validate UUIDs but does validate that the UUIDs are in the correct format.
    Format is 1 UUID per line, any line beginning with # is ignored.
    Whitespace is stripped.

    Arguments:
        filename: file name of the file containing UUIDs

    Returns:
        list of UUIDs or empty list of no UUIDs in file

    Raises:
        ValueError if UUID is not in correct format
    """

    uuid = []
    for line in stream:
        line = line.strip()
        if len(line) and line[0] != "#":
            if not re.match(f"^{UUID_PATTERN}$", line):
                raise ValueError(f"Invalid UUID: {line}")
            line = line.upper()
            uuid.append(line)
    return uuid
