import functools
import logging
import re
from typing import Any, Callable, cast

logger = logging.getLogger(__name__)

# ?todo: Recursive definition not working with pydantic
MappingDefinition0 = str | list[Any] | dict[str, Any]
MappingDefinition1 = str | list[MappingDefinition0] | dict[str, MappingDefinition0]
MappingDefinition = str | list[MappingDefinition1] | dict[str, MappingDefinition1]
# MappingDefinition = str | list["MappingDefinition"] | dict[str, "MappingDefinition"]


class StructuralMappingError(Exception):
	pass


def _get_repeat_pattern(mapping: list | tuple) -> tuple[int, str | None]:
	for idx, item in enumerate(mapping):
		if isinstance(item, str) and item.startswith("...repeat"):
			match = re.match(r"\.\.\.repeat\s+as\s+(?P<name>\w+)", item)
			if match:
				return idx, match.group("name")
			else:
				return idx, ""
	return len(mapping), None


def structural_map(
	raw_output: Any,
	mapping: MappingDefinition | None,
	skip_missing: bool = False,
) -> dict[str, float | int | str | list[float | int | str]]:
	if mapping is None:
		return cast(dict[str, Any], raw_output)

	def _recursive_map(
		raw_output: Any,
		mapping: Any,
		parsed_output: dict[str, Any] = {},
		can_repeat: bool = True,
	) -> dict[str, Any] | None:
		try:
			if isinstance(mapping, str):
				if mapping in ["...skip", ""]:
					# Skip this element
					return
				elif parsed_output.get(mapping) is None:
					parsed_output[mapping] = raw_output
				else:
					parsed_output[mapping] = (
						parsed_output[mapping]
						if isinstance(parsed_output[mapping], list)
						else [parsed_output[mapping]]
					)
					parsed_output[mapping].append(raw_output)
			elif isinstance(mapping, list) or isinstance(mapping, tuple):
				# Handle repeating patterns
				repeat_len, repeat_name = _get_repeat_pattern(mapping)
				if (
					repeat_len > len(raw_output)
					and "...repeat" not in mapping
					and not (len(raw_output) == 0 and repeat_name is not None)
				):
					raise StructuralMappingError(
						IndexError,
						raw_output,
						mapping,
						parsed_output,
					)
				if repeat_name is not None:
					if not can_repeat:
						raise StructuralMappingError(
							ValueError(
								"'...repeat' cannot be nested inside another repeating pattern."
							),
							raw_output,
							mapping,
							parsed_output,
						)
					if repeat_name == "":
						can_repeat = False
					else:
						parsed_output.setdefault(repeat_name, [])
				for raw_item_index, raw_item in enumerate(
					raw_output[: len(raw_output) // repeat_len * repeat_len]
				):
					mapping_index = raw_item_index % repeat_len
					repetition = raw_item_index // repeat_len

					if repeat_name is not None and repeat_name != "":
						if mapping_index == 0 and repetition >= len(
							parsed_output[repeat_name]
						):
							parsed_output[repeat_name].append({})

						_recursive_map(
							raw_item,
							mapping[mapping_index],
							parsed_output[repeat_name][repetition],
							can_repeat,
						)

					else:
						if repeat_name == "" and mapping[mapping_index] != "...skip":
							parsed_output.setdefault(mapping[mapping_index], [])
						_recursive_map(
							raw_item, mapping[mapping_index], parsed_output, can_repeat
						)

			else:
				if isinstance(raw_output, dict):
					for key in mapping.keys():
						_recursive_map(
							raw_output[key], mapping[key], parsed_output, can_repeat
						)
				else:
					for key in mapping.keys():
						_recursive_map(
							getattr(raw_output, key),
							mapping[key],
							parsed_output,
							can_repeat,
						)
		except (
			TypeError,  # wrong indexing
			IndexError,  # list indexing
			KeyError,  # dict indexing
			AttributeError,  # object attribute indexing
		) as e:
			if skip_missing:
				logger.debug(f"Skipping missing index `{e.args[0]}`")
			else:
				raise StructuralMappingError(e, raw_output, mapping, parsed_output)
		return parsed_output

	parsed_output = _recursive_map(raw_output, mapping) or {}

	return parsed_output


ObjectMapping = Callable[[Any], dict[str, Any] | list[dict[str, Any]]]


def structural_map_fx(
	mapping: MappingDefinition | None,
	skip_missing: bool = False,
) -> ObjectMapping:
	return functools.partial(
		structural_map,
		mapping=mapping,
		skip_missing=skip_missing,
	)
