import argparse
import datetime
import enum
import logging
import pathlib
import re

from typing import (
	ClassVar,
	Optional,
)

from .cache_db import cache_db_t
from .db import db_parser_t
from .models import mirror_config_t
from .pacman import pacman_t

logger = logging.getLogger(__name__)


class ArchiveAction(enum.Enum):
	list_dates = 'list-dates'
	list_packages = 'list-packages'
	show_versions = 'show-versions'
	sync = 'sync'


class archive_t:
	class constants_t:
		base_url: ClassVar[str] = 'https://archive.archlinux.org/repos/'
		href_re: ClassVar[re.Pattern[str]] = re.compile(r'href="(\d{4}/\d{2}/\d{2})/"')
		default_repos: ClassVar[list[str]] = ['core', 'extra', 'multilib']

	@staticmethod
	def list_remote_dates(
		base_url: Optional[str] = None,
	) -> list[str]:
		"""Scrape available date directories from the archive index page."""
		import urllib.request

		if base_url is None:
			base_url = archive_t.constants_t.base_url

		logger.info(dict(msg='fetching archive index', url=base_url))

		with urllib.request.urlopen(base_url) as resp:
			html = resp.read().decode('utf-8')

		dates: list[str] = []
		for m in archive_t.constants_t.href_re.finditer(html):
			dates.append(m.group(1))

		dates.sort(reverse=True)
		return dates

	@staticmethod
	def sync_date(
		date: str,
		cache_dir: pathlib.Path,
		cache_db: cache_db_t,
		repos: Optional[list[str]] = None,
		arch: str = 'x86_64',
	) -> None:
		if repos is None:
			repos = list(archive_t.constants_t.default_repos)

		mirror = mirror_config_t.from_archive_date(
			date=date,
			repos=repos,
			arch=arch,
		)

		db_dir = cache_dir / date
		db_dir.mkdir(parents=True, exist_ok=True)

		for repo_cfg in mirror.repos:
			db_url = '%s/%s.db' % (repo_cfg.url, repo_cfg.name)
			db_path = db_dir / ('%s.db' % repo_cfg.name)
			db_rel_path = '%s/%s.db' % (date, repo_cfg.name)

			if not db_path.exists():
				logger.info(
					dict(
						msg='downloading db',
						url=db_url,
						dest=str(db_path),
					)
				)
				pacman_t.download_db(db_url, db_path)
			else:
				logger.info(
					dict(
						msg='db already cached on disk',
						path=str(db_path),
					)
				)

			db_sha256 = cache_db_t.file_sha256(db_path)

			snapshot_id = cache_db.upsert_snapshot(
				date=date,
				repo=repo_cfg.name,
				arch=arch,
				db_sha256=db_sha256,
				db_rel_path=db_rel_path,
			)

			if cache_db.snapshot_package_count(snapshot_id) > 0:
				snap = cache_db.get_snapshot_by_id(snapshot_id)
				if snap is not None and snap.db_sha256 == db_sha256:
					logger.info(
						dict(
							msg='snapshot already in sqlite',
							date=date,
							repo=repo_cfg.name,
							snapshot_id=snapshot_id,
						)
					)
					continue

			index = db_parser_t.parse_db_path(db_path, repo_name=repo_cfg.name)

			cache_db.store_index(
				snapshot_id=snapshot_id,
				index=index,
			)

			logger.info(
				dict(
					msg='synced',
					date=date,
					repo=repo_cfg.name,
					packages=len(index.packages),
				)
			)

	@staticmethod
	def _parse_date(s: str) -> datetime.date:
		parts = s.split('/')
		if len(parts) == 3:
			return datetime.date(int(parts[0]), int(parts[1]), int(parts[2]))
		return datetime.date.fromisoformat(s)

	@staticmethod
	def _format_date(d: datetime.date) -> str:
		return '%04d/%02d/%02d' % (d.year, d.month, d.day)

	@staticmethod
	def sync_date_range(
		start_date: str,
		end_date: str,
		cache_dir: pathlib.Path,
		cache_db: cache_db_t,
		repos: Optional[list[str]] = None,
		arch: str = 'x86_64',
		step_days: int = 1,
	) -> None:
		start = archive_t._parse_date(start_date)
		end = archive_t._parse_date(end_date)
		step = datetime.timedelta(days=step_days)

		current = end
		while current >= start:
			date_str = archive_t._format_date(current)

			try:
				archive_t.sync_date(
					date=date_str,
					cache_dir=cache_dir,
					cache_db=cache_db,
					repos=repos,
					arch=arch,
				)
			except Exception:
				logger.warning(
					dict(
						msg='failed to sync date, skipping',
						date=date_str,
					),
					exc_info=True,
				)

			current -= step


def main(args: list[str]) -> int:
	archive_parser = argparse.ArgumentParser(
		prog='online-fxreader-pr34-archlinux archive',
	)
	archive_parser.add_argument(
		'action',
		choices=[o.value for o in ArchiveAction],
	)
	archive_parser.add_argument(
		'--cache-dir',
		dest='cache_dir',
		required=True,
		help='directory for cached .db files and sqlite database',
	)
	archive_parser.add_argument(
		'--repos',
		nargs='*',
		default=['core', 'extra', 'multilib'],
	)
	archive_parser.add_argument(
		'--arch',
		default='x86_64',
	)
	archive_parser.add_argument(
		'--date',
		default=None,
		help='single date (e.g. 2024/01/15) for sync',
	)
	archive_parser.add_argument(
		'--date-range',
		dest='date_range',
		nargs=2,
		metavar=('START', 'END'),
		default=None,
		help='date range for sync (e.g. 2024/01/01 2024/06/30)',
	)
	archive_parser.add_argument(
		'--date-step',
		dest='date_step',
		type=int,
		default=1,
		help='step in days when iterating date range, default 1',
	)
	archive_parser.add_argument(
		'--packages',
		nargs='*',
		default=None,
		help='package names for show-versions',
	)

	archive_options = archive_parser.parse_args(args)
	archive_options.action = ArchiveAction(archive_options.action)

	cache_dir = pathlib.Path(archive_options.cache_dir)
	cache_dir.mkdir(parents=True, exist_ok=True)

	db = cache_db_t(cache_dir / 'archlinux_cache.db')

	try:
		if archive_options.action is ArchiveAction.list_dates:
			if db.has_data():
				print('=== cached dates ===')
				for date_str in db.list_dates():
					print(date_str)

			print('=== remote dates ===')
			for date_str in archive_t.list_remote_dates():
				print(date_str)

		elif archive_options.action is ArchiveAction.list_packages:
			for row in db.package_count_per_date():
				print('%s  %d' % (row.date, row.count))

		elif archive_options.action is ArchiveAction.show_versions:
			if archive_options.packages is None or len(archive_options.packages) == 0:
				logger.error('--packages required for show-versions')
				return 1

			for row in db.get_package_versions(archive_options.packages):
				print('%s  %s  %s  %s' % (row.date, row.repo, row.name, row.version))

		elif archive_options.action is ArchiveAction.sync:
			if archive_options.date is not None:
				archive_t.sync_date(
					date=archive_options.date,
					cache_dir=cache_dir,
					cache_db=db,
					repos=archive_options.repos,
					arch=archive_options.arch,
				)
			elif archive_options.date_range is not None:
				archive_t.sync_date_range(
					start_date=archive_options.date_range[0],
					end_date=archive_options.date_range[1],
					cache_dir=cache_dir,
					cache_db=db,
					repos=archive_options.repos,
					arch=archive_options.arch,
					step_days=archive_options.date_step,
				)
			else:
				logger.error('sync requires --date or --date-range')
				return 1
		else:
			raise NotImplementedError
	finally:
		db.close()

	return 0
