#!/usr/bin/env python3
import glob
import io
import tempfile
import dataclasses
import pathlib
import sys
import subprocess
import os
import logging


import typing

from typing import (
	Optional,
	Any,
)

if typing.TYPE_CHECKING:
	from typing_extensions import (
		Self,
		BinaryIO,
	)

logger = logging.getLogger(__name__)


def toml_load(f: 'BinaryIO') -> Any:
	try:
		import tomllib

		return tomllib.load(f)
	except:
		pass

	try:
		import tomli

		return tomli.load(f)
	except:
		pass

	raise NotImplementedError


@dataclasses.dataclass
class PyProject:
	path: pathlib.Path
	dependencies: dict[str, list[str]]
	early_features: Optional[list[str]] = None
	pip_find_links: Optional[list[pathlib.Path]] = None
	runtime_libdirs: Optional[list[pathlib.Path]] = None
	runtime_preload: Optional[list[pathlib.Path]] = None
	requirements: dict[str, pathlib.Path] = dataclasses.field(default_factory=lambda: dict())


def pyproject_load(
	d: pathlib.Path,
) -> PyProject:
	with io.open(d, 'rb') as f:
		content = toml_load(f)

	assert isinstance(content, dict)

	dependencies: dict[str, list[str]] = dict()

	dependencies['default'] = content['project']['dependencies']

	if 'optional-dependencies' in content['project']:
		assert isinstance(content['project']['optional-dependencies'], dict)

		for k, v in content['project']['optional-dependencies'].items():
			assert isinstance(v, list)
			assert isinstance(k, str)

			dependencies[k] = v

	res = PyProject(
		path=d,
		dependencies=dependencies,
	)

	tool_name = 'online.fxreader.pr34'.replace('.', '-')

	if 'tool' in content and isinstance(content['tool'], dict) and tool_name in content['tool'] and isinstance(content['tool'][tool_name], dict):
		if 'early_features' in content['tool'][tool_name]:
			res.early_features = content['tool'][tool_name]['early_features']

		if 'pip_find_links' in content['tool'][tool_name]:
			res.pip_find_links = [d.parent / pathlib.Path(o) for o in content['tool'][tool_name]['pip_find_links']]

		if 'runtime_libdirs' in content['tool'][tool_name]:
			res.runtime_libdirs = [
				d.parent / pathlib.Path(o)
				# pathlib.Path(o)
				for o in content['tool'][tool_name]['runtime_libdirs']
			]

		if 'runtime_preload' in content['tool'][tool_name]:
			res.runtime_preload = [
				d.parent / pathlib.Path(o)
				# pathlib.Path(o)
				for o in content['tool'][tool_name]['runtime_preload']
			]

		if 'requirements' in content['tool'][tool_name]:
			assert isinstance(content['tool'][tool_name]['requirements'], dict)

			res.requirements = {
				k: d.parent / pathlib.Path(v)
				# pathlib.Path(o)
				for k, v in content['tool'][tool_name]['requirements'].items()
			}

	return res


@dataclasses.dataclass
class BootstrapSettings:
	env_path: pathlib.Path
	python_path: pathlib.Path
	base_dir: pathlib.Path
	python_version: Optional[str] = dataclasses.field(
		default_factory=lambda: os.environ.get(
			'PYTHON_VERSION',
			'%d.%d'
			% (
				sys.version_info.major,
				sys.version_info.minor,
			),
		).strip()
	)
	uv_args: list[str] = dataclasses.field(
		default_factory=lambda: os.environ.get(
			'UV_ARGS',
			'--offline',
		).split(),
	)

	@classmethod
	def get(
		cls,
		base_dir: Optional[pathlib.Path] = None,
	) -> 'Self':
		if base_dir is None:
			base_dir = pathlib.Path.cwd()

		env_path = base_dir / '.venv'
		python_path = env_path / 'bin' / 'python3'

		return cls(
			base_dir=base_dir,
			env_path=env_path,
			python_path=python_path,
		)


def env_bootstrap(
	bootstrap_settings: BootstrapSettings,
	pyproject: PyProject,
) -> None:
	pip_find_links: list[pathlib.Path] = []

	if not pyproject.pip_find_links is None:
		pip_find_links.extend(pyproject.pip_find_links)

	pip_find_links_args = sum(
		[
			[
				'-f',
				str(o),
			]
			for o in pip_find_links
		],
		[],
	)

	features: list[str] = []

	if pyproject.early_features:
		features.extend(pyproject.early_features)

	requirements_python_version: Optional[str] = None
	if not bootstrap_settings.python_version is None:
		requirements_python_version = bootstrap_settings.python_version.replace('.', '_')

	requirements_name = '_'.join(sorted(features))

	if requirements_python_version:
		requirements_name += '_' + requirements_python_version

	requirements_path: Optional[pathlib.Path] = None

	if requirements_name in pyproject.requirements:
		requirements_path = pyproject.requirements[requirements_name]
	else:
		requirements_path = pyproject.path.parent / 'requirements.txt'

	requirements_in: list[str] = []

	requirements_in.extend(['uv', 'pip', 'build', 'setuptools', 'meson-python', 'pybind11'])

	if pyproject.early_features:
		early_dependencies = sum([pyproject.dependencies[o] for o in pyproject.early_features], [])

		logger.info(
			dict(
				early_dependencies=early_dependencies,
			)
		)

		requirements_in.extend(early_dependencies)
		# if len(early_dependencies) > 0:
		#     subprocess.check_call([
		#         bootstrap_settings.python_path,
		#         '-m',
		#         'uv', 'pip', 'install',
		#         *pip_find_links_args,
		#         # '-f', str(pathlib.Path(__file__).parent / 'deps' / 'dist'),
		#         *bootstrap_settings.uv_args,
		#         *early_dependencies,
		#     ])

	if not requirements_path.exists():
		with tempfile.NamedTemporaryFile(
			mode='w',
			prefix='requirements',
			suffix='.in',
		) as f:
			f.write('\n'.join(requirements_in))
			f.flush()

			subprocess.check_call(
				[
					'uv',
					'pip',
					'compile',
					'--generate-hashes',
					*pip_find_links_args,
					# '-p',
					# bootstrap_settings.python_path,
					*bootstrap_settings.uv_args,
					'-o',
					str(requirements_path),
					f.name,
				]
			)

	uv_python_version: list[str] = []

	if not bootstrap_settings.python_version is None:
		uv_python_version.extend(
			[
				'-p',
				bootstrap_settings.python_version,
			]
		)

	subprocess.check_call(
		[
			'uv',
			'venv',
			*uv_python_version,
			*pip_find_links_args,
			# '--seed',
			*bootstrap_settings.uv_args,
			str(bootstrap_settings.env_path),
		]
	)

	subprocess.check_call(
		[
			'uv',
			'pip',
			'install',
			*pip_find_links_args,
			'-p',
			bootstrap_settings.python_path,
			'--require-hashes',
			*bootstrap_settings.uv_args,
			'-r',
			str(requirements_path),
		]
	)


def paths_equal(a: pathlib.Path | str, b: pathlib.Path | str) -> bool:
	return os.path.abspath(str(a)) == os.path.abspath(str(b))


def run(
	d: Optional[pathlib.Path] = None,
	cli_path: Optional[pathlib.Path] = None,
) -> None:
	if cli_path is None:
		cli_path = pathlib.Path(__file__).parent / 'cli.py'

	if d is None:
		d = pathlib.Path(__file__).parent / 'pyproject.toml'

	bootstrap_settings = BootstrapSettings.get()

	pyproject: PyProject = pyproject_load(d)

	logging.basicConfig(level=logging.INFO)

	if not bootstrap_settings.env_path.exists():
		env_bootstrap(
			bootstrap_settings=bootstrap_settings,
			pyproject=pyproject,
		)

	logger.info([sys.executable, sys.argv, bootstrap_settings.python_path])

	if not paths_equal(sys.executable, bootstrap_settings.python_path):
		os.execv(
			str(bootstrap_settings.python_path),
			[
				str(bootstrap_settings.python_path),
				*sys.argv,
			],
		)

	os.execv(
		str(bootstrap_settings.python_path),
		[
			str(bootstrap_settings.python_path),
			str(cli_path),
			*sys.argv[1:],
		],
	)


if __name__ == '__main__':
	run(
		d=pathlib.Path(__file__).parent / 'pyproject.toml',
		cli_path=pathlib.Path(__file__).parent / 'cli.py',
	)
