import dataclasses
import enum
import re
import logging

from typing import (
	ClassVar,
	Optional,
	Literal,
	Any,
)

logger = logging.getLogger(__name__)


class vercmp_t:
	class constants_t:
		epoch_re: ClassVar[re.Pattern[str]] = re.compile(r'^(\d+):(.+)$')
		rel_re: ClassVar[re.Pattern[str]] = re.compile(r'^(.+)-(\d+)$')
		segment_re: ClassVar[re.Pattern[str]] = re.compile(r'(\d+|[a-zA-Z]+)')

	@staticmethod
	def split_evr(version: str) -> tuple[int, str, str]:
		epoch = 0
		m = vercmp_t.constants_t.epoch_re.match(version)
		if m:
			epoch = int(m.group(1))
			version = m.group(2)

		rel = '0'
		m = vercmp_t.constants_t.rel_re.match(version)
		if m:
			version = m.group(1)
			rel = m.group(2)

		return (epoch, version, rel)

	@staticmethod
	def compare_segment(a: str, b: str) -> int:
		a_segments = vercmp_t.constants_t.segment_re.findall(a)
		b_segments = vercmp_t.constants_t.segment_re.findall(b)

		for sa, sb in zip(a_segments, b_segments):
			a_digit = sa.isdigit()
			b_digit = sb.isdigit()

			if a_digit and b_digit:
				ia, ib = int(sa), int(sb)
				if ia != ib:
					return 1 if ia > ib else -1
			elif a_digit:
				return 1
			elif b_digit:
				return -1
			else:
				if sa != sb:
					return 1 if sa > sb else -1

		if len(a_segments) != len(b_segments):
			return 1 if len(a_segments) > len(b_segments) else -1

		return 0

	@staticmethod
	def vercmp(a: str, b: str) -> int:
		if a == b:
			return 0

		a_epoch, a_ver, a_rel = vercmp_t.split_evr(a)
		b_epoch, b_ver, b_rel = vercmp_t.split_evr(b)

		if a_epoch != b_epoch:
			return 1 if a_epoch > b_epoch else -1

		ret = vercmp_t.compare_segment(a_ver, b_ver)
		if ret != 0:
			return ret

		return vercmp_t.compare_segment(a_rel, b_rel)


class constraint_op_t(enum.Enum):
	eq = '='
	ge = '>='
	le = '<='
	gt = '>'
	lt = '<'


class package_constraint_t:
	class constants_t:
		constraint_re: ClassVar[re.Pattern[str]] = re.compile(r'^([a-zA-Z0-9@._+\-]+?)(?:(>=|<=|>|<|=)(.+))?$')

	def __init__(
		self,
		name: str,
		op: Optional[constraint_op_t] = None,
		version: Optional[str] = None,
	) -> None:
		self.name = name
		self.op = op
		self.version = version

	def __eq__(self, other: Any) -> bool:
		if not isinstance(other, package_constraint_t):
			return NotImplemented
		return self.name == other.name and self.op == other.op and self.version == other.version

	def __hash__(self) -> int:
		return hash((self.name, self.op, self.version))

	def __repr__(self) -> str:
		if self.op is None or self.version is None:
			return 'package_constraint_t(%r)' % self.name
		return 'package_constraint_t(%r, %r, %r)' % (self.name, self.op.value, self.version)

	@staticmethod
	def parse(s: str) -> 'package_constraint_t':
		m = package_constraint_t.constants_t.constraint_re.match(s.strip())
		if not m:
			raise ValueError('invalid constraint: %s' % s)

		name = m.group(1)
		op_str = m.group(2)
		version = m.group(3)

		op: Optional[constraint_op_t] = None
		if op_str:
			op = constraint_op_t(op_str)

		return package_constraint_t(
			name=name,
			op=op,
			version=version,
		)

	def satisfied_by(self, version: str) -> bool:
		if self.op is None or self.version is None:
			return True

		constraint_ver = self.version
		candidate_ver = version

		if '-' not in constraint_ver and '-' in candidate_ver:
			candidate_ver = vercmp_t.constants_t.rel_re.sub(r'\1', candidate_ver)

		cmp = vercmp_t.vercmp(candidate_ver, constraint_ver)

		if self.op is constraint_op_t.eq:
			return cmp == 0
		elif self.op is constraint_op_t.ge:
			return cmp >= 0
		elif self.op is constraint_op_t.le:
			return cmp <= 0
		elif self.op is constraint_op_t.gt:
			return cmp > 0
		elif self.op is constraint_op_t.lt:
			return cmp < 0
		else:
			raise NotImplementedError

	def to_str(self) -> str:
		if self.op is None or self.version is None:
			return self.name
		return '%s%s%s' % (self.name, self.op.value, self.version)


@dataclasses.dataclass
class package_desc_t:
	name: str
	version: str
	desc: str = ''
	filename: str = ''
	csize: int = 0
	isize: int = 0
	md5sum: str = ''
	sha256sum: str = ''
	url: str = ''
	arch: str = ''
	builddate: int = 0
	packager: str = ''
	license: list[str] = dataclasses.field(default_factory=lambda: list[str]())
	depends: list[str] = dataclasses.field(default_factory=lambda: list[str]())
	optdepends: list[str] = dataclasses.field(default_factory=lambda: list[str]())
	makedepends: list[str] = dataclasses.field(default_factory=lambda: list[str]())
	checkdepends: list[str] = dataclasses.field(default_factory=lambda: list[str]())
	provides: list[str] = dataclasses.field(default_factory=lambda: list[str]())
	conflicts: list[str] = dataclasses.field(default_factory=lambda: list[str]())
	replaces: list[str] = dataclasses.field(default_factory=lambda: list[str]())
	groups: list[str] = dataclasses.field(default_factory=lambda: list[str]())
	base: str = ''

	def parsed_depends(self) -> list[package_constraint_t]:
		return [package_constraint_t.parse(d) for d in self.depends]

	def parsed_provides(self) -> list[package_constraint_t]:
		return [package_constraint_t.parse(p) for p in self.provides]

	def parsed_conflicts(self) -> list[package_constraint_t]:
		return [package_constraint_t.parse(c) for c in self.conflicts]


@dataclasses.dataclass
class repo_config_t:
	name: str
	url: str


@dataclasses.dataclass
class mirror_config_t:
	repos: list[repo_config_t] = dataclasses.field(default_factory=lambda: list[repo_config_t]())
	arch: str = 'x86_64'

	@staticmethod
	def from_archive_date(
		date: str,
		repos: Optional[list[str]] = None,
		arch: str = 'x86_64',
	) -> 'mirror_config_t':
		if repos is None:
			repos = ['core', 'extra', 'multilib']

		base_url = 'https://archive.archlinux.org/repos/%s' % date

		return mirror_config_t(
			repos=[
				repo_config_t(
					name=r,
					url='%s/%s/os/%s' % (base_url, r, arch),
				)
				for r in repos
			],
			arch=arch,
		)

	@staticmethod
	def from_mirror_url(
		mirror_url: str,
		repos: Optional[list[str]] = None,
		arch: str = 'x86_64',
	) -> 'mirror_config_t':
		if repos is None:
			repos = ['core', 'extra', 'multilib']

		return mirror_config_t(
			repos=[
				repo_config_t(
					name=r,
					url='%s/%s/os/%s' % (mirror_url.rstrip('/'), r, arch),
				)
				for r in repos
			],
			arch=arch,
		)


@dataclasses.dataclass
class repo_index_t:
	name: str
	packages: dict[str, package_desc_t] = dataclasses.field(default_factory=lambda: dict[str, package_desc_t]())
	provides_index: dict[str, list[str]] = dataclasses.field(default_factory=lambda: dict[str, list[str]]())
	groups_index: dict[str, list[str]] = dataclasses.field(default_factory=lambda: dict[str, list[str]]())

	def build_provides_index(self) -> None:
		self.provides_index = {}
		self.groups_index = {}
		for pkg_name, pkg in self.packages.items():
			for prov in pkg.provides:
				prov_constraint = package_constraint_t.parse(prov)
				if prov_constraint.name not in self.provides_index:
					self.provides_index[prov_constraint.name] = []
				self.provides_index[prov_constraint.name].append(pkg_name)
			for group in pkg.groups:
				if group not in self.groups_index:
					self.groups_index[group] = []
				self.groups_index[group].append(pkg_name)


class resolution_strategy_t(enum.Enum):
	upgrade_all = 'upgrade-all'
	pin_referenced = 'pin-referenced'


@dataclasses.dataclass
class compile_options_t:
	packages: list[str] = dataclasses.field(default_factory=lambda: list[str]())
	index_url: Optional[str] = None
	archive_date: Optional[str] = None
	offline: bool = False
	no_cache: bool = False
	generate_hashes: bool = False
	repos: list[str] = dataclasses.field(default_factory=lambda: ['core', 'extra', 'multilib'])
	arch: str = 'x86_64'
	cache_dir: Optional[str] = None
	reference: Optional[str] = None
	resolution_strategy: resolution_strategy_t = resolution_strategy_t.upgrade_all


@dataclasses.dataclass
class compile_entry_t:
	name: str
	version: str
	filename: str
	repo: str
	url: str
	sha256: str = ''
	depends: list[str] = dataclasses.field(default_factory=lambda: list[str]())


class compile_result_t:
	@dataclasses.dataclass
	class res_t:
		entries: list[compile_entry_t] = dataclasses.field(default_factory=lambda: list[compile_entry_t]())
		txt: str = ''

		def to_txt(self) -> str:
			lines: list[str] = []
			for e in sorted(self.entries, key=lambda x: x.name):
				line = '%s==%s' % (e.name, e.version)
				if e.sha256:
					line += ' --hash=sha256:%s' % e.sha256
				lines.append('# %s' % e.url if e.url else '# %s/%s' % (e.repo, e.filename))
				lines.append(line)
			return '\n'.join(lines)
