import dataclasses
import logging

from typing import (
	Optional,
	Any,
)

from .models import (
	package_desc_t,
	package_constraint_t,
	repo_index_t,
	vercmp_t,
)

logger = logging.getLogger(__name__)


class resolver_t:
	class error_t:
		class not_found_t(Exception):
			def __init__(self, name: str) -> None:
				self.name = name
				super().__init__('package not found: %s' % name)

		class conflict_t(Exception):
			def __init__(self, pkg_a: str, pkg_b: str, constraint: str) -> None:
				self.pkg_a = pkg_a
				self.pkg_b = pkg_b
				self.constraint = constraint
				super().__init__('conflict: %s conflicts with %s (%s)' % (pkg_a, pkg_b, constraint))

		class unsatisfied_t(Exception):
			def __init__(self, parent: str, dep: str) -> None:
				self.parent = parent
				self.dep = dep
				super().__init__('unsatisfied dependency: %s requires %s' % (parent, dep))

	@dataclasses.dataclass
	class res_t:
		resolved: dict[str, package_desc_t] = dataclasses.field(default_factory=lambda: dict[str, package_desc_t]())
		resolution_order: list[str] = dataclasses.field(default_factory=lambda: list[str]())

	@staticmethod
	def _find_provider(
		constraint: package_constraint_t,
		indices: list[repo_index_t],
	) -> Optional[tuple[package_desc_t, str]]:
		for index in indices:
			if constraint.name in index.packages:
				pkg = index.packages[constraint.name]
				if constraint.satisfied_by(pkg.version):
					return (pkg, index.name)

		for index in indices:
			if constraint.name in index.provides_index:
				for provider_name in index.provides_index[constraint.name]:
					pkg = index.packages[provider_name]
					for prov in pkg.parsed_provides():
						if prov.name == constraint.name:
							if constraint.version is None or prov.version is None:
								return (pkg, index.name)
							if constraint.satisfied_by(prov.version):
								return (pkg, index.name)

		return None

	@staticmethod
	def resolve(
		packages: list[str],
		indices: list[repo_index_t],
		skip_installed: Optional[set[str]] = None,
	) -> 'resolver_t.res_t':
		if skip_installed is None:
			skip_installed = set()

		result = resolver_t.res_t()
		visited: set[str] = set()
		stack: list[tuple[package_constraint_t, Optional[str]]] = []

		for pkg_str in packages:
			constraint = package_constraint_t.parse(pkg_str)
			stack.append((constraint, None))

		while len(stack) > 0:
			constraint, parent = stack.pop()

			if constraint.name in visited:
				if constraint.name in result.resolved:
					pkg = result.resolved[constraint.name]
					if not constraint.satisfied_by(pkg.version):
						raise resolver_t.error_t.unsatisfied_t(
							parent=parent or '<root>',
							dep=constraint.to_str(),
						)
				continue

			if constraint.name in skip_installed:
				visited.add(constraint.name)
				continue

			found = resolver_t._find_provider(constraint, indices)

			if found is None:
				exists = any(constraint.name in idx.packages or constraint.name in idx.provides_index for idx in indices)
				if exists:
					raise resolver_t.error_t.unsatisfied_t(
						parent=parent or '<root>',
						dep=constraint.to_str(),
					)
				raise resolver_t.error_t.not_found_t(constraint.name)

			pkg, repo_name = found

			if pkg.name in visited:
				if pkg.name in result.resolved and constraint.op is not None:
					resolved_pkg = result.resolved[pkg.name]
					if constraint.name == resolved_pkg.name:
						if not constraint.satisfied_by(resolved_pkg.version):
							raise resolver_t.error_t.unsatisfied_t(
								parent=parent or '<root>',
								dep=constraint.to_str(),
							)
					else:
						matched = False
						for prov in resolved_pkg.parsed_provides():
							if prov.name == constraint.name:
								if prov.version is not None and constraint.satisfied_by(prov.version):
									matched = True
									break
								elif prov.version is None:
									matched = True
									break
						if not matched:
							raise resolver_t.error_t.unsatisfied_t(
								parent=parent or '<root>',
								dep=constraint.to_str(),
							)
				continue

			visited.add(pkg.name)
			visited.add(constraint.name)

			result.resolved[pkg.name] = pkg
			result.resolution_order.append(pkg.name)

			for conflict in pkg.parsed_conflicts():
				if conflict.name in result.resolved:
					resolved_version = result.resolved[conflict.name].version
					if conflict.satisfied_by(resolved_version):
						raise resolver_t.error_t.conflict_t(
							pkg_a=pkg.name,
							pkg_b=conflict.name,
							constraint=conflict.to_str(),
						)

			for dep in pkg.parsed_depends():
				if dep.name not in visited and dep.name not in skip_installed:
					stack.append((dep, pkg.name))

		return result
