import itertools
from string import Template
from typing import (
    Optional,
    Iterable,
    Any,
    Tuple,
    Mapping,
    Sequence,
    FrozenSet,
    Container,
    Union,
)

from debputy import DEBPUTY_DOC_ROOT_DIR
from debputy.commands.debputy_cmd.output import OutputStyle
from debputy.manifest_parser.declarative_parser import (
    DeclarativeMappingInputParser,
    DeclarativeNonMappingInputParser,
    AttributeDescription,
    BASIC_SIMPLE_TYPES,
)
from debputy.manifest_parser.parser_data import ParserContextData
from debputy.manifest_parser.tagging_types import TypeMapping
from debputy.manifest_parser.util import AttributePath, unpack_type
from debputy.plugin.api.impl_types import (
    DebputyPluginMetadata,
    DeclarativeInputParser,
    DispatchingObjectParser,
    ListWrappedDeclarativeInputParser,
    InPackageContextParser,
    PluginProvidedTypeMapping,
)
from debputy.plugin.api.spec import (
    ParserDocumentation,
    reference_documentation,
    undocumented_attr,
    DebputyIntegrationMode,
    ALL_DEBPUTY_INTEGRATION_MODES,
    TypeMappingExample,
)
from debputy.util import assume_not_none, _error, _warn


def _provide_placeholder_parser_doc(
    parser_doc: Optional[ParserDocumentation],
    attributes: Iterable[str],
) -> ParserDocumentation:
    if parser_doc is None:
        parser_doc = reference_documentation()
    changes = {}
    if parser_doc.attribute_doc is None:
        changes["attribute_doc"] = [undocumented_attr(attr) for attr in attributes]

    if changes:
        return parser_doc.replace(**changes)
    return parser_doc


def doc_args_for_parser_doc(
    rule_name: str,
    declarative_parser: DeclarativeInputParser[Any],
    plugin_metadata: DebputyPluginMetadata,
    *,
    manifest_format_url: Optional[str] = None,
) -> Tuple[Mapping[str, str], ParserDocumentation]:
    attributes: Iterable[str]
    if isinstance(declarative_parser, DeclarativeMappingInputParser):
        attributes = declarative_parser.source_attributes.keys()
    else:
        attributes = []
    if manifest_format_url is None:
        manifest_format_url = f"{DEBPUTY_DOC_ROOT_DIR}/MANIFEST-FORMAT.md"
    doc_args = {
        "RULE_NAME": rule_name,
        "MANIFEST_FORMAT_DOC": manifest_format_url,
        "PLUGIN_NAME": plugin_metadata.plugin_name,
    }
    parser_doc = _provide_placeholder_parser_doc(
        declarative_parser.inline_reference_documentation,
        attributes,
    )
    return doc_args, parser_doc


def render_attribute_doc(
    parser: Any,
    attributes: Mapping[str, "AttributeDescription"],
    required_attributes: FrozenSet[str],
    conditionally_required_attributes: FrozenSet[FrozenSet[str]],
    parser_doc: ParserDocumentation,
    doc_args: Mapping[str, str],
    *,
    rule_name: str = "<unset>",
    is_root_rule: bool = False,
    is_interactive: bool = False,
) -> Iterable[Tuple[FrozenSet[str], Sequence[str]]]:
    provided_attribute_docs = (
        parser_doc.attribute_doc if parser_doc.attribute_doc is not None else []
    )

    for attr_doc in assume_not_none(provided_attribute_docs):
        attr_description = attr_doc.description
        rendered_doc = []

        for parameter in sorted(attr_doc.attributes):
            parameter_details = attributes.get(parameter)
            if parameter_details is not None:
                source_name = parameter_details.source_attribute_name
                describe_type = parameter_details.type_validator.describe_type()
            else:
                assert isinstance(parser, DispatchingObjectParser)
                source_name = parameter
                subparser = parser.parser_for(source_name).parser
                if isinstance(subparser, InPackageContextParser):
                    if is_interactive:
                        describe_type = "PackageContext"
                    else:
                        rule_prefix = rule_name if not is_root_rule else ""
                        describe_type = f"PackageContext (chains to `{rule_prefix}::{subparser.manifest_attribute_path_template}`)"

                elif isinstance(subparser, DispatchingObjectParser):
                    if is_interactive:
                        describe_type = "Object"
                    else:
                        rule_prefix = rule_name if not is_root_rule else ""
                        describe_type = f"Object (see `{rule_prefix}::{subparser.manifest_attribute_path_template}`)"
                elif isinstance(subparser, DeclarativeMappingInputParser):
                    describe_type = "<Type definition not implemented yet>"  # TODO: Derive from subparser
                elif isinstance(subparser, DeclarativeNonMappingInputParser):
                    describe_type = (
                        subparser.alt_form_parser.type_validator.describe_type()
                    )
                else:
                    describe_type = f"<Unknown: Non-introspectable subparser - {subparser.__class__.__name__}>"

            if source_name in required_attributes:
                req_str = "required"
            elif any(source_name in s for s in conditionally_required_attributes):
                req_str = "conditional"
            else:
                req_str = "optional"
            rendered_doc.append(f"`{source_name}` ({req_str}): {describe_type}")

        if attr_description:
            rendered_doc.append("")
            attr_doc_rendered = _render_template(
                f"attr docs for {rule_name}",
                attr_description,
                doc_args,
            )
            rendered_doc.extend(
                line for line in attr_doc_rendered.splitlines(keepends=False)
            )
            rendered_doc.append("")
        yield attr_doc.attributes, rendered_doc


def _render_template(name: str, template_str: str, params: Mapping[str, str]) -> str:
    try:
        return Template(template_str).substitute(params)
    except KeyError as e:
        _warn(f"Render issue: {str(e)}")
        _error(f"Failed to render {name}: Missing key {e.args[0]}")
    except ValueError as e:
        _warn(f"Render issue: {str(e)}")
        _error(f"Failed to render {name}")


def _render_integration_mode(
    expected_modes: Optional[Container[DebputyIntegrationMode]],
) -> Optional[str]:
    if expected_modes:
        allowed_modes = set()
        for mode in sorted(ALL_DEBPUTY_INTEGRATION_MODES):
            if mode in expected_modes:
                allowed_modes.add(mode)

        if allowed_modes == ALL_DEBPUTY_INTEGRATION_MODES:
            restriction = "any integration mode"
        else:
            restriction = ", ".join(sorted(allowed_modes))
    else:
        restriction = "any integration mode"

    return f"Integration mode availability: {restriction}"


def render_rule(
    rule_name: str,
    declarative_parser: DeclarativeInputParser[Any],
    plugin_metadata: DebputyPluginMetadata,
    color_base: OutputStyle,
    *,
    is_root_rule: bool = False,
    include_ref_doc_link: bool = True,
    include_alt_format: bool = True,
    base_heading_level: int = 1,
    manifest_format_url: Optional[str] = None,
    show_integration_mode: bool = True,
) -> str:
    doc_args, parser_doc = doc_args_for_parser_doc(
        "the manifest root" if is_root_rule else rule_name,
        declarative_parser,
        plugin_metadata,
        manifest_format_url=manifest_format_url,
    )
    t = _render_template(
        f"title of {rule_name}",
        assume_not_none(parser_doc.title),
        doc_args,
    )
    body = _render_template(
        f"body of {rule_name}",
        assume_not_none(parser_doc.description),
        doc_args,
    ).rstrip()
    r = [
        color_base.heading(t, base_heading_level),
        "",
        body,
        "",
    ]

    if show_integration_mode:
        allowed_integration_modes = _render_integration_mode(
            declarative_parser.expected_debputy_integration_mode
        )
    else:
        allowed_integration_modes = None
    alt_form_parser = getattr(declarative_parser, "alt_form_parser", None)
    is_list_wrapped = False
    unwrapped_parser = declarative_parser
    if isinstance(declarative_parser, ListWrappedDeclarativeInputParser):
        is_list_wrapped = True
        unwrapped_parser = declarative_parser.delegate

    if isinstance(
        unwrapped_parser, (DeclarativeMappingInputParser, DispatchingObjectParser)
    ):

        if isinstance(unwrapped_parser, DeclarativeMappingInputParser):
            attributes = unwrapped_parser.source_attributes
            required = unwrapped_parser.input_time_required_parameters
            conditionally_required = unwrapped_parser.at_least_one_of
            mutually_exclusive = unwrapped_parser.mutually_exclusive_attributes
        else:
            attributes = {}
            required = frozenset()
            conditionally_required = frozenset()
            mutually_exclusive = frozenset()
        if is_list_wrapped:
            r.append("List where each element has the following attributes:")
        else:
            r.append("Attributes:")

        rendered_attr_doc = render_attribute_doc(
            unwrapped_parser,
            attributes,
            required,
            conditionally_required,
            parser_doc,
            doc_args,
            is_root_rule=is_root_rule,
            rule_name=rule_name,
            is_interactive=False,
        )
        for _, rendered_doc in rendered_attr_doc:
            prefix = " - "
            for line in rendered_doc:
                if line:
                    r.append(f"{prefix}{line}")
                else:
                    r.append("")
                prefix = "   "

        if (
            bool(conditionally_required)
            or bool(mutually_exclusive)
            or any(pd.conflicting_attributes for pd in attributes.values())
        ):
            r.append("")
            if is_list_wrapped:
                r.append(
                    "This rule enforces the following restrictions on each element in the list:"
                )
            else:
                r.append("This rule enforces the following restrictions:")

            if conditionally_required or mutually_exclusive:
                all_groups = list(
                    itertools.chain(conditionally_required, mutually_exclusive)
                )
                seen = set()
                for g in all_groups:
                    if g in seen:
                        continue
                    seen.add(g)
                    anames = "`, `".join(sorted(g))
                    is_mx = g in mutually_exclusive
                    is_cr = g in conditionally_required
                    if is_mx and is_cr:
                        r.append(f" - The rule must use exactly one of: `{anames}`")
                    elif is_cr:
                        r.append(f" - The rule must use at least one of: `{anames}`")
                    else:
                        assert is_mx
                        r.append(
                            f" - The following attributes are mutually exclusive: `{anames}`"
                        )

            if mutually_exclusive or any(
                pd.conflicting_attributes for pd in attributes.values()
            ):
                for parameter, parameter_details in sorted(attributes.items()):
                    source_name = parameter_details.source_attribute_name
                    conflicts = set(parameter_details.conflicting_attributes)
                    for mx in mutually_exclusive:
                        if parameter in mx and mx not in conditionally_required:
                            conflicts |= mx
                    if conflicts:
                        conflicts.discard(parameter)
                        cnames = "`, `".join(
                            sorted(
                                attributes[a].source_attribute_name for a in conflicts
                            )
                        )
                        r.append(
                            f" - The attribute `{source_name}` cannot be used with any of: `{cnames}`"
                        )
        r.append("")
    if include_alt_format and alt_form_parser is not None:
        # FIXME: Mapping[str, Any] ends here, which is ironic given the headline.
        r.append(
            f"Non-mapping format: {alt_form_parser.type_validator.describe_type()}"
        )
        alt_parser_desc = parser_doc.alt_parser_description
        if alt_parser_desc:
            r.extend(
                f"   {line}"
                for line in alt_parser_desc.format(**doc_args).splitlines(
                    keepends=False
                )
            )
        r.append("")

    if allowed_integration_modes:
        r.append(allowed_integration_modes)

    if include_ref_doc_link:
        if declarative_parser.reference_documentation_url is not None:
            r.append(
                f"Reference documentation: {declarative_parser.reference_documentation_url}"
            )
        else:
            r.append(
                "Reference documentation: No reference documentation link provided by the plugin"
            )
    elif allowed_integration_modes:
        # Better spacing in the generated docs, but it looks weird with this newline
        # in `debputy plugin show p-m-r ...`
        r.append("")

    return "\n".join(r)


def render_multiline_documentation(
    documentation: str,
    *,
    first_line_prefix: str = "Documentation: ",
    following_line_prefix: str = " ",
) -> Iterable[str]:
    current_prefix = first_line_prefix
    result = []
    for line in documentation.splitlines(keepends=False):
        if line.isspace():
            if not current_prefix.isspace():
                result.append(current_prefix.rstrip())
                current_prefix = following_line_prefix
            else:
                result.append("")
            continue
        result.append(f"{current_prefix}{line}")
        current_prefix = following_line_prefix
    return result


def _render_type_example(
    type_mapping: TypeMapping[Any, Any],
    output_style: OutputStyle,
    parser_context: ParserContextData,
    example: TypeMappingExample,
    *,
    recover_from_broken_examples: bool,
) -> Tuple[str, bool]:
    attr_path = AttributePath.builtin_path()["Render Request"]
    v = _render_value(example.source_input)
    try:
        type_mapping.mapper(
            example.source_input,
            attr_path,
            parser_context,
        )
    except RuntimeError:
        if not recover_from_broken_examples:
            raise
        return (
            output_style.colored(v, fg="red") + " [Example value could not be parsed]",
            True,
        )
    return output_style.colored(v, fg="green"), False


def render_source_type(t: Any) -> str:
    _, origin_type, args = unpack_type(t, False)
    if origin_type == Union:
        return " | ".join(render_source_type(st) for st in args)
    name = BASIC_SIMPLE_TYPES.get(t)
    if name is not None:
        return name
    try:
        return t.__name__
    except AttributeError:
        return str(t)


def render_type_mapping(
    pptm: PluginProvidedTypeMapping,
    output_style: OutputStyle,
    parser_context: ParserContextData,
    *,
    recover_from_broken_examples: bool = False,
    base_heading_level: int = 1,
) -> str:
    type_mapping = pptm.mapped_type
    target_type = type_mapping.target_type
    ref_doc = pptm.reference_documentation
    desc = ref_doc.description if ref_doc is not None else None
    examples = ref_doc.examples if ref_doc is not None else tuple()
    base_type = render_source_type(type_mapping.source_type)
    lines = [
        output_style.heading(
            f"Type Mapping: {target_type.__name__} [{base_type}]", base_heading_level
        ),
        "",
    ]

    if desc is not None:
        lines.extend(
            render_multiline_documentation(
                desc,
                first_line_prefix="",
                following_line_prefix="",
            )
        )
    else:
        lines.append("No documentation provided.")

    if examples:
        had_issues = False
        lines.append("")
        lines.append(output_style.heading("Example values", base_heading_level + 1))
        lines.append("")
        for no, example in enumerate(examples, start=1):
            v, i = _render_type_example(
                type_mapping,
                output_style,
                parser_context,
                example,
                recover_from_broken_examples=recover_from_broken_examples,
            )
            if i and recover_from_broken_examples:
                lines.append(
                    output_style.colored("Broken example: ", fg="red")
                    + f"Provided example input ({v})"
                    + " caused an exception when parsed. Please file a bug against the plugin."
                    + " Use --debug/DEBPUTY_DEBUG=1 to see the stack trace"
                )
            lines.append(f" * {v}")
            if i:
                had_issues = True
    else:
        had_issues = False

    if had_issues:
        lines.append("")
        lines.append(
            output_style.colored(
                "Examples had issues. Please file a bug against the plugin", fg="red"
            )
        )
        lines.append("")
        lines.append("Use --debug/DEBPUTY_DEBUG=1 to see the stacktrace")

    return "\n".join(lines)


def _render_value(v: Any) -> str:
    if isinstance(v, str) and '"' not in v:
        return f'"{v}"'
    return str(v)
