from pathlib import Path
from typing import Optional

import pytz
from click import Choice, Context, Parameter, ParamType

from dvt.config.utils import normalize_warn_error_options, parse_cli_yaml_string
from dvt.event_time.sample_window import SampleWindow
from dvt.events import ALL_EVENT_NAMES
from dvt.exceptions import OptionNotYamlDictError, ValidationError
from dbt_common.exceptions import DbtValidationError
from dbt_common.helper_types import WarnErrorOptionsV2


class PathWithExpandUser(ParamType):
    """Click Path type that expands ~ before resolving/exists check."""

    name = "path"

    def __init__(self, exists: bool = True):
        self.exists = exists

    def convert(self, value, param, ctx):
        if value is None:
            return value
        path = Path(value).expanduser().resolve()
        if self.exists and not path.exists():
            self.fail(f"Path '{path}' does not exist.", param, ctx)
        return path


class YAML(ParamType):
    """The Click YAML type. Converts YAML strings into objects."""

    name = "YAML"

    def convert(self, value, param, ctx):
        # assume non-string values are a problem
        if not isinstance(value, str):
            self.fail(f"Cannot load YAML from type {type(value)}", param, ctx)
        try:
            param_option_name = param.opts[0] if param.opts else param.name
            return parse_cli_yaml_string(value, param_option_name.strip("-"))
        except (ValidationError, DbtValidationError, OptionNotYamlDictError):
            self.fail(f"String '{value}' is not valid YAML", param, ctx)


class Package(ParamType):
    """The Click STRING type. Converts string into dict with package name and version.
    Example package:
        package-name@1.0.0
        package-name
    """

    name = "NewPackage"

    def convert(self, value, param, ctx):
        # assume non-string values are a problem
        if not isinstance(value, str):
            self.fail(f"Cannot load Package from type {type(value)}", param, ctx)
        try:
            package_name, package_version = value.split("@")
            return {"name": package_name, "version": package_version}
        except ValueError:
            return {"name": value, "version": None}


class WarnErrorOptionsType(YAML):
    """The Click WarnErrorOptions type. Converts YAML strings into objects."""

    name = "WarnErrorOptionsType"

    def convert(self, value, param, ctx):
        # this function is being used by param in click
        warn_error_options = super().convert(value, param, ctx)
        normalize_warn_error_options(warn_error_options)

        return WarnErrorOptionsV2(
            error=warn_error_options.get("error", []),
            warn=warn_error_options.get("warn", []),
            silence=warn_error_options.get("silence", []),
            valid_error_names=ALL_EVENT_NAMES,
        )


class Truthy(ParamType):
    """The Click Truthy type.  Converts strings into a "truthy" type"""

    name = "TRUTHY"

    def convert(self, value, param, ctx):
        # assume non-string / non-None values are a problem
        if not isinstance(value, (str, None)):
            self.fail(f"Cannot load TRUTHY from type {type(value)}", param, ctx)

        if value is None or value.lower() in ("0", "false", "f"):
            return None
        else:
            return value


class ChoiceTuple(Choice):
    name = "CHOICE_TUPLE"

    def convert(self, value, param, ctx):
        if not isinstance(value, str):
            for value_item in value:
                super().convert(value_item, param, ctx)
        else:
            super().convert(value, param, ctx)

        return value


class SampleType(ParamType):
    name = "SAMPLE"

    def convert(
        self, value, param: Optional[Parameter], ctx: Optional[Context]
    ) -> Optional[SampleWindow]:
        if value is None:
            return None

        if isinstance(value, str):
            try:
                # Try and identify if it's a "dict" or a "str"
                if value.lstrip()[0] == "{":
                    param_option_name: str = param.opts[0] if param.opts else param.name  # type: ignore
                    parsed_dict = parse_cli_yaml_string(value, param_option_name.strip("-"))
                    sample_window = SampleWindow.from_dict(parsed_dict)
                    sample_window.start = sample_window.start.replace(tzinfo=pytz.UTC)
                    sample_window.end = sample_window.end.replace(tzinfo=pytz.UTC)
                    return sample_window
                else:
                    return SampleWindow.from_relative_string(value)
            except Exception as e:
                self.fail(e.__str__(), param, ctx)
        else:
            self.fail(f"Cannot load SAMPLE_WINDOW from type {type(value)}", param, ctx)
