"""
Colourful & descriptive errors for xmlable
- Clear messages
- Trace for parsing
"""

from dataclasses import dataclass
from typing import Any, Iterable
from termcolor import colored

from ._utils import typename, AnyType


def trace_note(trace: list[str], arrow_c: str, node_c: str):
    return colored(" > ", arrow_c).join(
        map(lambda x: colored(x, node_c), trace)
    )


@dataclass
class XErrorCtx:
    trace: list[str]

    def next(self, node: str):
        return XErrorCtx(trace=self.trace + [node])


# TODO: Custom backtrace to point to location in the file
class XError(Exception):
    def __init__(
        self,
        short: str,
        what: str,
        why: str,
        ctx: XErrorCtx | None = None,
        notes: Iterable[str] = [],
    ):
        super().__init__(colored(short, "red", attrs=["blink"]))
        self.add_note(colored("What:  " + what, "blue"))
        self.add_note(colored("Why:   " + why, "yellow"))
        if ctx is not None:
            self.add_note(
                colored("Where: ", "magenta")
                + trace_note(ctx.trace, "light_magenta", "light_cyan")
            )
        for note in notes:
            self.add_note(note)


class ErrorTypes:
    @staticmethod
    def NonXMlifiedType(t_name: str) -> XError:
        return XError(
            short="Non XMlified Type",
            what=f"You attempted to use {t_name} in an xmlified class, but {t_name} is not xmlified",
            why=f"All types used in an xmlified class must be xmlified",
        )

    @staticmethod
    def InvalidData(ctx: XErrorCtx, val: Any, t_name: str) -> XError:
        return XError(
            short="Invalid Data",
            what=f"Could not validate {val} as a valid {t_name}",
            why=f"Produced xml must be valid",
            ctx=ctx,
        )

    @staticmethod
    def ParseFailure(
        ctx: XErrorCtx, text: str | None, t_name: str, caught: Exception
    ) -> XError:
        return XError(
            short="Parse Failure",
            what=f"Failed to parse {text} as a {t_name} with error: \n {caught}",
            why=f"This error implies the xml is not validated against the current xsd, or there is a bug in this type's parser",
            ctx=ctx,
        )

    @staticmethod
    def UnexpectedTag(
        ctx: XErrorCtx, expected_name: str, struct_name: str, tag_found: str
    ) -> XError:
        return XError(
            short="Unexpected Tag",
            what=f"Expected {expected_name} but found {tag_found}",
            why=f"This is a {struct_name} that contains 0..n elements of {expected_name} and no other elements",
            ctx=ctx,
        )

    @staticmethod
    def IncorrectType(
        ctx: XErrorCtx, expected_len: int, struct_name: str, val: Any, name: str
    ) -> XError:
        return XError(
            short="Incorrect Type",
            what=f"You have provided {len(val)} values {val} for {name}, but {name} is a {struct_name} that takes only {expected_len} values",
            why=f"In order to generate xml, the values provided need to be the correct types",
            ctx=ctx,
        )

    @staticmethod
    def IncorrectElementTag(
        ctx: XErrorCtx,
        struct_name: str,
        tag_name: str,
        elem_index: int,
        tag_expected: str,
        tag_found: str,
    ) -> XError:
        return XError(
            short="Incorrect Element Tag",
            what=f"While parsing {struct_name} {tag_name} we expected element {elem_index} to be {tag_expected}, but found {tag_found}",
            why=f"The xml representation for {struct_name} requires the correct names in the correct order",
            ctx=ctx,
        )

    @staticmethod
    def DuplicateItem(
        ctx: XErrorCtx, struct_name: str, tag: str, item: str
    ) -> XError:
        return XError(
            short=f"Duplicate item in {struct_name}",
            what=f"In {tag} the item {item} is present more than once",
            why=f"A set can only contain unique items",
            ctx=ctx,
        )

    @staticmethod
    def InvalidDictionaryItem(
        ctx: XErrorCtx,
        expected_tag: str,
        expected_key: str,
        expected_val: str,
        dict_tag: str,
        item_tag: str,
    ) -> XError:
        return XError(
            short="Invalid item in dictionary",
            what=f"An unexpected item with {dict_tag} is in dictionary {item_tag}",
            why=f"Each item must have tag {expected_tag} with children {expected_key} and {expected_val}",
            ctx=ctx,
        )

    @staticmethod
    def InvalidVariant(
        ctx: XErrorCtx,
        name: str,
        expected_types: list[AnyType],
        found_type: AnyType | None,
        found_value: Any,
    ) -> XError:
        types = " | ".join(map(str, expected_types))
        return XError(
            short=f"Datatype not in Union",
            what=f"{name} is a union of {types}, which does not contain {found_type} (you provided: {found_value})",
            why=f"... uuuh, its a union?",
            ctx=ctx,
        )

    @staticmethod
    def MultipleVariants(ctx: XErrorCtx, variant_names: list[str]) -> XError:
        return XError(
            short="Multiple union variants present",
            what=f"variants {', '.join(variant_names)} are present",
            why=f"A union can only be one variant at a time",
            ctx=ctx,
        )

    @staticmethod
    def ParseInvalidVariant(
        ctx: XErrorCtx, tag: str, named_variants: list[str], found_variant: str
    ) -> XError:
        return XError(
            short="Invalid Variant",
            what=f"The union {tag} can contain variants {', '.join(named_variants)}, but you have used {found_variant}",
            why=f"Only valid variants can be parsed",
            ctx=ctx,
        )

    @staticmethod
    def NoneIsSome(ctx: XErrorCtx, name: str, val: Any) -> XError:
        return XError(
            short="None object is not None",
            what=f"{name} contains value {val} which is not None",
            why="A None type object can only contain none",
            ctx=ctx,
        )

    @staticmethod
    def NotADataclass(cls: AnyType) -> XError:
        cls_name: str = typename(cls)
        return XError(
            short="Non-Dataclass",
            what=f"{cls_name} is not a dataclass",
            why=f"xmlify uses dataclasses to get fields",
            ctx=XErrorCtx([cls_name]),
            notes=[f"\nTry:\n@xmlify\n@dataclass\nclass {cls_name}:"],
        )

    @staticmethod
    def ReservedAttribute(cls: AnyType, attr_name: str) -> XError:
        cls_name: str = typename(cls)
        return XError(
            short=f"Reserved Attribute",
            what=f"{cls_name}.{attr_name} is used by xmlify, so it cannot be a field of the class",
            why=f"xmlify aguments {cls_name} by adding methods it can then use for xsd, xml generation and parsing",
            ctx=XErrorCtx([cls_name]),
        )

    @staticmethod
    def CommentAttribute(cls: AnyType) -> XError:
        cls_name: str = typename(cls)
        return XError(
            short=f"Comment Attribute",
            what=f"xmlifed classes cannot use comment as an attribute",
            why=f"comment is used as a tag name for comments by lxml, so comments inserted on xml generation could conflict",
            ctx=XErrorCtx([cls_name]),
        )

    @staticmethod
    def NonMemberTag(
        ctx: XErrorCtx, cls: AnyType, tag: str, name: str
    ) -> XError:
        cls_name: str = typename(cls)
        return XError(
            short="Non member tag",
            what=f"In {tag} {cls_name}.{name} could not be found.",
            why=f"All members, including {cls_name}.{name} must be present",
            ctx=ctx,
        )

    @staticmethod
    def MissingAttribute(
        cls: AnyType, required_attrs: set[str], missing_attr: str
    ) -> XError:
        cls_name: str = typename(cls)
        return XError(
            short="Missing Attribute",
            what=f"The attribute {missing_attr} is missing from {cls_name}",
            why=f"To be manual_xmlified the attributes: {', '.join(required_attrs)} are required. Try using help(IXmlify)",
            ctx=XErrorCtx([cls_name]),
        )

    @staticmethod
    def DependencyCycle(cycle: list[AnyType]) -> XError:
        return XError(
            short="Dependency Cycle in XSD",
            what=f"There is a cycle: {'<-'.join(map(str, cycle))}",
            why="The XSDs for classes are written to the .xsd file in dependency order",
        )

    @staticmethod
    def NotXmlified(cls: AnyType) -> XError:
        cls_name: str = typename(cls)
        return XError(
            short="Not Xmlified",
            what=f"{cls_name} is not xmlified, and hence cannot have an associated parser",
            why=f"the .xsd(...) method is required to write_xsd",
            notes=[f"To fix, try:\n@xmlify\n@dataclass\nclass {cls_name}: ..."],
        )
