import argparse
import os
from typing import Any, List, Optional

from strictdoc.cli.command_parser_builder import (
    CommandParserBuilder,
    SDocArgumentParser,
)
from strictdoc.helpers.auto_described import auto_described
from strictdoc.helpers.cast import assert_cast
from strictdoc.helpers.net import is_valid_host


class CLIValidationError(Exception):
    pass


class ImportReqIFCommandConfig:
    def __init__(
        self,
        input_path: str,
        output_path: str,
        profile: str,
        reqif_enable_mid: bool,
        reqif_import_markup: Optional[str],
    ):
        self.input_path: str = input_path
        self.output_path: str = output_path
        self.profile: Optional[str] = profile
        self.reqif_enable_mid: bool = reqif_enable_mid
        self.reqif_import_markup: Optional[str] = reqif_import_markup


class ManageAutoUIDCommandConfig:
    def __init__(
        self,
        *,
        input_path: str,
        config_path: Optional[str],
        include_sections: bool,
    ):
        self.input_path: str = input_path
        self._config_path: Optional[str] = config_path
        self.include_sections: bool = include_sections

    def get_path_to_config(self) -> str:
        path_to_input_dir: str = self.input_path
        if os.path.isfile(path_to_input_dir):
            path_to_input_dir = os.path.dirname(path_to_input_dir)
        path_to_config = (
            self._config_path
            if self._config_path is not None
            else path_to_input_dir
        )
        return path_to_config

    def validate(self) -> None:
        if self._config_path is not None and not os.path.exists(
            self._config_path
        ):
            raise CLIValidationError(
                "Provided path to a configuration file does not exist: "
                f"{self._config_path}"
            )


class ImportExcelCommandConfig:
    def __init__(
        self, input_path: str, output_path: str, parser: SDocArgumentParser
    ) -> None:
        self.input_path: str = input_path
        self.output_path: str = output_path
        self.parser: SDocArgumentParser = parser


@auto_described
class ServerCommandConfig:
    def __init__(
        self,
        *,
        input_path: str,
        output_path: Optional[str],
        config_path: Optional[str],
        reload: bool,
        host: Optional[str],
        port: Optional[int],
    ):
        self._input_path: str = input_path
        self.output_path: Optional[str] = output_path
        self._config_path: Optional[str] = config_path
        self.reload: bool = reload
        self.host: Optional[str] = host
        self.port: Optional[int] = port

    def get_full_input_path(self) -> str:
        return os.path.abspath(self._input_path)

    def get_path_to_config(self) -> str:
        return (
            self._config_path
            if self._config_path is not None
            else self._input_path
        )

    def validate(self) -> None:
        if not os.path.exists(self._input_path):
            raise CLIValidationError(
                f"Provided input path does not exist: {self._input_path}"
            )

        if self._config_path is not None and not os.path.exists(
            self._config_path
        ):
            raise CLIValidationError(
                "Provided path to a configuration file does not exist: "
                f"{self._config_path}"
            )

        if (host_ := self.host) is not None:
            if not is_valid_host(host_):
                raise CLIValidationError(
                    f"Provided 'host' argument is not a valid host: {host_}"
                )


@auto_described
class ExportCommandConfig:
    def __init__(
        self,
        input_paths: List[str],
        output_dir: Optional[str],
        config_path: Optional[str],
        project_title: Optional[str],
        formats: List[str],
        fields: List[str],
        generate_bundle_document: bool,
        no_parallelization: bool,
        enable_mathjax: bool,
        included_documents: bool,
        filter_requirements: Optional[str],
        filter_sections: Optional[str],
        reqif_profile: Optional[str],
        reqif_multiline_is_xhtml: bool,
        reqif_enable_mid: bool,
        view: Optional[str],
        chromedriver: Optional[str],
    ):
        assert isinstance(input_paths, list), f"{input_paths}"
        self.input_paths: List[str] = input_paths
        self.output_dir: Optional[str] = output_dir
        self._config_path: Optional[str] = config_path
        self.project_title: Optional[str] = project_title
        self.formats: List[str] = formats
        self.fields: List[str] = fields
        self.generate_bundle_document: bool = generate_bundle_document
        self.no_parallelization: bool = no_parallelization
        self.enable_mathjax: bool = enable_mathjax
        self.included_documents: bool = included_documents
        self.filter_requirements: Optional[str] = filter_requirements
        self.filter_sections: Optional[str] = filter_sections
        self.reqif_profile: Optional[str] = reqif_profile
        self.reqif_multiline_is_xhtml: bool = reqif_multiline_is_xhtml
        self.reqif_enable_mid: bool = reqif_enable_mid
        self.view: Optional[str] = view
        self.chromedriver: Optional[str] = chromedriver

    def get_path_to_config(self) -> str:
        # FIXME: The control flow can be improved.
        path_to_input_dir: str = self.input_paths[0]
        if os.path.isfile(path_to_input_dir):
            path_to_input_dir = os.path.dirname(path_to_input_dir)
        path_to_config = (
            self._config_path
            if self._config_path is not None
            else path_to_input_dir
        )
        return path_to_config

    def validate(self) -> None:
        for idx_, input_path_ in enumerate(self.input_paths.copy()):
            if not os.path.exists(input_path_):
                raise CLIValidationError(
                    f"Provided input path does not exist: {input_path_}"
                )
            if not os.path.isabs(input_path_):
                self.input_paths[idx_] = os.path.abspath(input_path_).rstrip(
                    "/"
                )
        if self._config_path is not None:
            if not os.path.exists(self._config_path):
                raise CLIValidationError(
                    "Provided path to a configuration file does not exist: "
                    f"{self._config_path}"
                )


class DumpGrammarCommandConfig:
    def __init__(self, output_file: str):
        self.output_file: str = output_file


class DiffCommandConfig:
    def __init__(
        self,
        path_to_lhs_tree: str,
        path_to_rhs_tree: str,
        output_dir: Optional[str],
    ):
        self.path_to_lhs_tree: str = path_to_lhs_tree
        self.path_to_rhs_tree: str = path_to_rhs_tree
        self.output_dir: Optional[str] = output_dir


class SDocArgsParser:
    def __init__(self, args: argparse.Namespace):
        self.args: argparse.Namespace = args

    def is_debug_mode(self) -> bool:
        return assert_cast(self.args.debug, bool)

    @property
    def is_about_command(self) -> bool:
        return str(self.args.command) == "about"

    @property
    def is_passthrough_command(self) -> bool:
        return str(self.args.command) == "passthrough"

    @property
    def is_export_command(self) -> bool:
        return str(self.args.command) == "export"

    @property
    def is_import_command_reqif(self) -> bool:
        return (
            str(self.args.command) == "import"
            and str(self.args.import_format) == "reqif"
        )

    @property
    def is_import_command_excel(self) -> bool:
        return (
            str(self.args.command) == "import"
            and str(self.args.import_format) == "excel"
        )

    @property
    def is_server_command(self) -> bool:
        return str(self.args.command) == "server"

    @property
    def is_dump_grammar_command(self) -> bool:
        return str(self.args.command) == "dump-grammar"

    @property
    def is_version_command(self) -> bool:
        return str(self.args.command) == "version"

    @property
    def is_diff_command(self) -> bool:
        return str(self.args.command) == "diff"

    @property
    def is_manage_autouid_command(self) -> bool:
        return (
            str(self.args.command) == "manage"
            and str(self.args.subcommand) == "auto-uid"
        )

    def get_export_config(self) -> ExportCommandConfig:
        project_title: Optional[str] = self.args.project_title

        return ExportCommandConfig(
            self.args.input_paths,
            self.args.output_dir,
            self.args.config,
            project_title,
            self.args.formats,
            self.args.fields,
            self.args.generate_bundle_document,
            self.args.no_parallelization,
            self.args.enable_mathjax,
            self.args.included_documents,
            self.args.filter_requirements,
            self.args.filter_sections,
            self.args.reqif_profile,
            self.args.reqif_multiline_is_xhtml,
            self.args.reqif_enable_mid,
            self.args.view,
            self.args.chromedriver,
        )

    def get_import_config_reqif(self, _: Any) -> ImportReqIFCommandConfig:
        return ImportReqIFCommandConfig(
            self.args.input_path,
            self.args.output_path,
            self.args.profile,
            self.args.reqif_enable_mid,
            self.args.reqif_import_markup,
        )

    def get_manage_autouid_config(self) -> ManageAutoUIDCommandConfig:
        return ManageAutoUIDCommandConfig(
            input_path=self.args.input_path,
            config_path=self.args.config,
            include_sections=self.args.include_sections,
        )

    def get_import_config_excel(self, _: Any) -> ImportExcelCommandConfig:
        return ImportExcelCommandConfig(
            self.args.input_path, self.args.output_path, self.args.parser
        )

    def get_server_config(self) -> ServerCommandConfig:
        return ServerCommandConfig(
            input_path=self.args.input_path,
            output_path=self.args.output_path,
            config_path=self.args.config,
            reload=self.args.reload,
            host=self.args.host,
            port=self.args.port,
        )

    def get_dump_grammar_config(self) -> DumpGrammarCommandConfig:
        return DumpGrammarCommandConfig(output_file=self.args.output_file)

    def get_diff_config(self) -> DiffCommandConfig:
        return DiffCommandConfig(
            path_to_lhs_tree=self.args.path_to_lhs_tree,
            path_to_rhs_tree=self.args.path_to_rhs_tree,
            output_dir=self.args.output_dir,
        )


def create_sdoc_args_parser(
    testing_args: Optional[argparse.Namespace] = None,
) -> SDocArgsParser:
    args = testing_args
    if not args:
        builder = CommandParserBuilder()
        parser = builder.build()
        args = parser.parse_args()
    return SDocArgsParser(args)
