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


from typing import (
	Optional,
	Any,
	cast,
	Type,
	TypeVar,
	Callable,
)
from typing_extensions import (
	Self,
	BinaryIO,
	overload,
)

logger = logging.getLogger(__name__)


def toml_load(f: BinaryIO) -> Any:
	try:
		tomllib = importlib.import_module('tomllib')

		return cast(
			Callable[[Any], Any],
			getattr(
				tomllib,
				'load',
			),
		)(f)
	except ModuleNotFoundError:
		pass

	try:
		import tomli

		return tomli.load(f)
	except ModuleNotFoundError:
		pass

	raise NotImplementedError


@dataclasses.dataclass
class PyProject:
	@dataclasses.dataclass
	class Module:
		name: str
		meson: Optional[pathlib.Path] = None
		tool: dict[str, Any] = dataclasses.field(default_factory=lambda: dict())

	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())

	modules: list[Module] = dataclasses.field(
		default_factory=lambda: [],
	)

	tool: dict[str, Any] = dataclasses.field(
		default_factory=lambda: dict(),
	)


Key = TypeVar('Key')
Value = TypeVar('Value')


@overload
def check_dict(
	value: Any,
	KT: Type[Key],
	VT: Type[Value],
) -> dict[Key, Value]: ...


@overload
def check_dict(
	value: Any,
	KT: Type[Key],
) -> dict[Key, Any]: ...


def check_dict(
	value: Any,
	KT: Type[Key],
	VT: Optional[Type[Value]] = None,
) -> dict[Key, Value]:
	assert isinstance(value, dict)
	value2 = cast(dict[Any, Any], value)

	VT_class: Optional[type[Any]] = None

	if not VT is None:
		if not typing.get_origin(VT) is None:
			VT_class = cast(type[Any], typing.get_origin(VT))
		else:
			VT_class = VT

	assert all([isinstance(k, KT) and (VT_class is None or isinstance(v, VT_class)) for k, v in value2.items()])

	if VT is None:
		return cast(
			dict[Key, Any],
			value,
		)
	else:
		return cast(
			dict[Key, Value],
			value,
		)


@overload
def check_list(
	value: Any,
	VT: Type[Value],
) -> list[Value]: ...


@overload
def check_list(
	value: Any,
) -> list[Any]: ...


def check_list(
	value: Any,
	VT: Optional[Type[Value]] = None,
) -> list[Value] | list[Any]:
	assert isinstance(value, list)
	value2 = cast(list[Any], value)

	assert all([(VT is None or isinstance(o, VT)) for o in value2])

	if VT is None:
		return cast(
			list[Any],
			value,
		)
	else:
		return cast(
			list[Value],
			value,
		)


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 check_dict(
			check_dict(
				check_dict(
					content,
					str,
					# Any,
				)['project'],
				str,
				# Any,
			)['optional-dependencies'],
			str,
			list[Any],
		).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:
		res.tool = check_dict(
			content['tool'],
			str,
		)

	if 'tool' in content and isinstance(content['tool'], dict) and tool_name in content['tool'] and isinstance(content['tool'][tool_name], dict):
		pr34_tool = check_dict(
			check_dict(
				content['tool'],
				str,
			)[tool_name],
			str,
		)

		if 'early_features' in pr34_tool:
			res.early_features = pr34_tool['early_features']

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

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

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

		if 'requirements' in pr34_tool:
			res.requirements = {
				k: d.parent / pathlib.Path(v)
				# pathlib.Path(o)
				for k, v in check_dict(pr34_tool['requirements'], str, str).items()
			}

		if 'modules' in pr34_tool:
			modules = check_list(pr34_tool['modules'])
			# res.modules = []

			for o in modules:
				assert isinstance(o, dict)
				assert 'name' in o and isinstance(o['name'], str)

				module = PyProject.Module(
					name=o['name'],
				)

				if 'meson' in o:
					assert 'meson' in o and isinstance(o['meson'], str)

					module.meson = pathlib.Path(o['meson'])

				if 'tool' in o:
					module.tool.update(
						check_dict(
							o['tool'],
							str,
						)
					)

				res.modules.append(module)

	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()
	)
	pip_check_conflicts: Optional[bool] = dataclasses.field(
		default_factory=lambda: os.environ.get('PIP_CHECK_CONFLICTS', json.dumps(True)) in [json.dumps(True)],
	)
	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: Optional[pathlib.Path] = None
		if 'ENV_PATH' in os.environ:
			env_path = pathlib.Path(os.environ['ENV_PATH'])
		else:
			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,
		)


class requirements_name_get_t:
	@dataclasses.dataclass
	class res_t:
		not_compiled: pathlib.Path
		compiled: pathlib.Path
		name: str


def requirements_name_get(
	source_dir: pathlib.Path,
	python_version: Optional[str],
	features: list[str],
	requirements: dict[str, pathlib.Path],
) -> requirements_name_get_t.res_t:
	requirements_python_version: Optional[str] = None
	if not python_version is None:
		requirements_python_version = 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 requirements:
		requirements_path = requirements[requirements_name]
	else:
		requirements_path = source_dir / 'requirements.txt'

	requirements_path_in = requirements_path.parent / (requirements_path.stem + '.in')

	requirements_in: list[str] = []

	return requirements_name_get_t.res_t(
		not_compiled=requirements_path_in,
		compiled=requirements_path,
		name=requirements_name,
	)


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
		],
		cast(list[str], []),
	)

	features: list[str] = []

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

	requirements_name_get_res = requirements_name_get(
		python_version=bootstrap_settings.python_version,
		features=features,
		requirements=pyproject.requirements,
		source_dir=pyproject.path.parent,
	)
	requirements_path = requirements_name_get_res.compiled

	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], cast(list[str], []))

		logger.info(
			dict(
				requirements_name_get_res=requirements_name_get_res,
				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,
		#     ])

	uv_python_version: list[str] = []

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

	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',
					*uv_python_version,
					'--generate-hashes',
					*pip_find_links_args,
					# '-p',
					# bootstrap_settings.python_path,
					*bootstrap_settings.uv_args,
					'-o',
					str(requirements_path),
					f.name,
				]
			)

	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',
			*uv_python_version,
			*pip_find_links_args,
			# '-p',
			# bootstrap_settings.python_path,
			'--require-hashes',
			*bootstrap_settings.uv_args,
			'-r',
			str(requirements_path),
		]
	)

	if bootstrap_settings.pip_check_conflicts:
		subprocess.check_call(
			[
				bootstrap_settings.python_path,
				'-m',
				'online.fxreader.pr34.commands',
				'pip_check_conflicts',
			]
		)


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()
