from __future__ import annotations

import re
import typing
from abc import ABC
from pathlib import Path
from typing import *

from .common import BaseCategory, Severity, ViolationPrototype
from .types import AnyPath


if (typing.TYPE_CHECKING):
    from flake8.violation import Violation as Flake8Violation

AnyViolation: TypeAlias = Union[str, 'Flake8Violation', ViolationPrototype[Any]]

class BaseViolationParser[C: BaseCategory]:
    REGISTRY: ClassVar[Dict[str, BaseViolationParser]] = dict()
    
    provider_name: str
    ViolationCategory: Type[C]
    severity_mapping: Dict[C, Severity]
    
    default_severity: Severity = Severity.Warning
    violation_code_number_base: int = 100
    
    @classmethod
    def parse_module_name(cls, filename: AnyPath):
        return Path(filename) \
            .absolute() \
            .relative_to(Path('.').absolute()) \
            .with_suffix('') \
            .as_posix() \
            .replace('/', '.') \
    
    def parse_violation_base(self, error_code: str) -> ViolationPrototype[None] | None:
        parsed = re.fullmatch(r'(?P<provider>\w+?)(?P<number>\d+)', error_code)
        if not parsed:
            return None
        
        result: ViolationPrototype[None] = ViolationPrototype(error_code, parsed.group('provider'), int(parsed.group('number')))
        return result
    
    def parse_violation_category(self, violation: ViolationPrototype[Any]) -> ViolationPrototype[C]:
        violation.provider = self.provider_name
        
        try:
            violation.category = self.ViolationCategory(violation.error_code_number // self.violation_code_number_base * self.violation_code_number_base)
        except (ValueError, LookupError):
            violation.category = None
        
        violation.severity = self.severity_mapping.get(violation.category, self.default_severity)
        
        return violation
    
    def parse_violation_extras(self, violation: ViolationPrototype[C]) -> ViolationPrototype[C]:
        return violation
    
    def parse_violation_code(self, violation: AnyViolation) -> str:
        match violation:
            case str(code):
                error_code = code
            case ViolationPrototype(error_code=code):
                error_code = code
            case flake8_violation if flake8_violation.__class__.__name__ == 'Violation':  # type: Flake8Violation
                error_code = flake8_violation.code
            case _:
                error_code = str(violation)
        
        return error_code
    
    def parse_violation(self, violation: AnyViolation) -> ViolationPrototype[C] | None:
        error_code = self.parse_violation_code(violation)
        violation = violation and self.parse_violation_base(error_code)
        violation = violation and self.parse_violation_category(violation)
        violation = violation and self.parse_violation_extras(violation)
        return violation
    
    @classmethod
    def parse_using_registry(cls, violation: AnyViolation, *, optimistic: bool = True) -> ViolationPrototype[Any] | None:
        parser = SimpleViolationParser.INSTANCE
        error_code = parser.parse_violation_code(violation)
        violation = violation and parser.parse_violation_base(error_code)
        parser = BaseViolationParser.REGISTRY.get(violation.error_code_class, parser if (optimistic) else None)
        return parser and parser.parse_violation(violation)
    
    @classmethod
    def register(cls, *error_classes: str):
        instance = cls()
        instance.register_violation_class(*error_classes)
    
    @classmethod
    def register_decorator[T: BaseViolationParser](cls, *error_classes: str) -> Callable[[Type[T]], Type[T]]:
        def decorator(parser_cls: Type[T]) -> Type[T]:
            parser_cls.register(*error_classes)
            return parser_cls
        return decorator
    
    def register_violation_class(self, *error_classes: str) -> None:
        for error_class in error_classes:
            assert error_class not in self.REGISTRY, f"Cannot re-register error class '{error_class}'"
            BaseViolationParser.REGISTRY[error_class] = self


class SingleCategoryViolationParser[C: BaseCategory](BaseViolationParser, ABC):
    category_name: str
    category_number: int
    
    @classmethod
    def make_enum(cls) -> Type[C]:
        enum_name = cls.__name__.replace('ViolationParser', '') + 'Category'
        enum_cls = BaseCategory(enum_name, { cls.category_name: cls.category_number })
        return cast(Type[C], enum_cls)
    
    def __init_subclass__(cls, **kwargs):
        if (not hasattr(cls, 'ViolationCategory')):
            cls.ViolationCategory = cls.make_enum()
        if (not hasattr(cls, 'severity_mapping')):
            cls.severity_mapping = { cls.ViolationCategory[cls.category_name]: cls.default_severity }
        
        super().__init_subclass__()


class SimpleViolationParser(SingleCategoryViolationParser):
    provider_name = 'linter-adapters'
    category_name = 'Violation'
    category_number = 000

SimpleViolationParser.INSTANCE = SimpleViolationParser()


__all__ = \
[
    'BaseViolationParser',
    'SingleCategoryViolationParser',
    'SimpleViolationParser',
]
