"""Support for handling datapane app config"""
import dataclasses as dc
import os
import re
from pathlib import Path
from typing import ClassVar, List, Optional

import dacite
import yaml
from boltons.strutils import slugify
from typing_extensions import Self

from datapane.client import log
from datapane.client.exceptions import DPClientError
from datapane.common import SDict, utf_read_text

# script paths
DATAPANE_YAML = Path("datapane.yaml")
PYPROJECT_TOML = Path("pyproject.toml")
DEFAULT_PY = Path("dp_app.py")
DEFAULT_IPYNB = Path("dp_app.ipynb")
re_check_name = re.compile(r"^[\S ]+$")

# TODO - look at other libs
#  - https://lidatong.github.io/dataclasses-json/ (or marshmallow)
#  - https://github.com/edaniszewski/bison
#  - https://pydantic-docs.helpmanual.io/

# TODO
#  - add support to extract elements from inline and pyproject
#  - use datapane.py by default as script
#  - convert notebook (use 1st markdown as docstring?)


def generate_name(postfix: str) -> str:
    cwd = os.path.basename(os.getcwd())
    dir_name = slugify(cwd, lower=False, delim=" ").title()
    return f"{dir_name} {postfix}"


# TODO(obsolete) - not really needed now, can remove in future
def validate_name(x: str):
    if re_check_name.match(x) is None:
        raise DPClientError(f"'{x}' is not a valid service name")


@dc.dataclass
class DatapaneCfg:
    """Wrapper around the datapane app config"""

    name: str = "datapane"
    # relative path to script
    script: Path = dc.field(default_factory=lambda: DEFAULT_IPYNB if DEFAULT_IPYNB.exists() else DEFAULT_PY)
    config: dc.InitVar[Path] = None
    proj_dir: ClassVar[Path] = None

    parameters: List[SDict] = dc.field(default_factory=list)
    pre_commands: List[str] = dc.field(default_factory=list)
    post_commands: List[str] = dc.field(default_factory=list)

    # metadata
    description: str = "Datapane App"
    source_url: str = ""
    project: Optional[str] = None
    environment: Optional[str] = None

    # build options
    include: List[str] = dc.field(default_factory=list)
    exclude: List[str] = dc.field(default_factory=list)
    requirements: List[str] = dc.field(default_factory=list)

    def __post_init__(self, config: Optional[Path]):
        validate_name(self.name)

        # TODO - support running config/script from another dir with abs paths
        # all paths are relative and we're running from the same dir
        if config:
            assert config.parent == self.script.parent == Path("."), "All files must be in the main project directory"
        # we must be in the project dir for now
        # TODO - move this logic to create_initial
        self.proj_dir = self.script.resolve(strict=False).parent  # type: ignore
        assert os.getcwd() == os.path.abspath(self.proj_dir), "Please run from source directory"

        # # config and script dir must be in same dir
        # if config:
        #     assert config.resolve(strict=True).parent == self.proj_dir, \
        #         "Config and Script directory must be in same directory"

        # validate config
        # if self.parameters:
        #     config_schema = json.loads(ir.read_text("datapane.resources", "app_parameter_def.schema.json"))
        #     jsonschema.validate(self.parameters, config_schema)

    @classmethod
    def create_initial(cls, config_file: Path = None, script: Path = None, **kw) -> Self:
        raw_config = {}

        if config_file:
            assert config_file.exists()
        else:
            config_file = DATAPANE_YAML

        if config_file.exists():
            # read config from the yaml file
            log.debug(f"Reading datapane config file at {config_file}")
            with config_file.open("r") as f:
                raw_config = yaml.safe_load(f)
        elif PYPROJECT_TOML.exists():
            # TODO - implement pyproject parsing
            log.warning("pyproject.toml found but not currently supported - ignoring")
            raw_config = {}
        elif script:
            # we don't have a default config - perhaps in the script file
            # TODO - try read config from source-code
            abs_script = config_file.parent / script
            if script.suffix == ".ipynb":
                log.debug("Converting notebook")
                mod_code = extract_py_notebook(abs_script)
            else:
                mod_code = utf_read_text(abs_script)
            log.debug("Reading config from python script/notebook")
            log.debug(mod_code)

        # overwrite config with command-line options
        if script:
            raw_config.update(script=script)
        raw_config.update(kw)
        readme = config_file.parent / "README.md"
        if readme.exists():
            raw_config["description"] = utf_read_text(readme)
        elif "description" not in raw_config:
            raw_config["description"] = cls.description

        dp_cfg = dacite.from_dict(cls, data=raw_config, config=dacite.Config(cast=[Path]))
        return dp_cfg

    @classmethod
    def create(cls, **raw_config) -> Self:
        return dacite.from_dict(cls, data=raw_config, config=dacite.Config(cast=[Path]))

    @staticmethod
    def exists() -> bool:
        check_files = [DATAPANE_YAML, PYPROJECT_TOML, DEFAULT_PY, DEFAULT_IPYNB]
        return any(x.exists() for x in check_files)

    def to_dict(self) -> SDict:
        d = dc.asdict(self)
        # TODO - make script a getter/setter
        d["script"] = str(d["script"])  # this is hacky - need a better DTO-conversion method
        build_fields = {"include", "exclude"}
        return {k: v for k, v in d.items() if k not in build_fields}


def extract_py_notebook(in_file: Path) -> str:
    from datapane.ipython.utils import extract_py_notebook

    return extract_py_notebook(in_file)
