import contextlib
import glob
import pathlib
import sys
import enum
import dataclasses
import subprocess
import tempfile

import unittest.mock
import logging
import typing

from typing_extensions import (
	cast,
	Protocol,
)

if typing.TYPE_CHECKING:
	import pip._internal.commands.show
	import pip._internal.commands.download
	import pip._internal.cli.main_parser
	import pip._internal.models.index
	import pip._internal.utils.temp_dir
	import pip._internal.cli.main
	import pip._internal.network.download
	import pip._internal.resolution.base
	import pip._internal.resolution.resolvelib.resolver
	import pip._internal.operations.prepare
	import pip._internal.index.package_finder
	from pip._internal.req.req_install import InstallRequirement

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

logger = logging.getLogger(__name__)


class pip_show_t:
	class res_t:
		class package_info_t:
			location: str


def pip_show(
	argv: list[str],
) -> list[
	# 'pip._internal.commands.show._PackageInfo'
	pip_show_t.res_t.package_info_t,
]:
	import pip._internal.commands.show

	return cast(
		list[pip_show_t.res_t.package_info_t],
		list(
			pip._internal.commands.show.search_packages_info(
				argv,
			)
		),
	)


class pip_resolve_t:
	class build_package_finder_t(Protocol):
		def __call__(
			self,
			options: Any,
			session: Any,
			target_python: Any,
			ignore_requires_python: Any,
		) -> 'pip._internal.index.package_finder.PackageFinder': ...

	class complete_partial_requirements_t(Protocol):
		def __call__(
			self,
			_self: 'pip._internal.resolution.resolvelib.resolver.Resolver',
			partially_downloaded_reqs: Iterable['InstallRequirement',],
			parallel_builds: bool = False,
		) -> None: ...

	class kwargs_t:
		class mode_t(enum.Enum):
			copy_paste = 'copy_paste'
			monkey_patch = 'monkey_patch'
			uv_pip_freeze = 'uv_pip_freeze'
			uv_pip_compile = 'uv_pip_compile'

	@dataclasses.dataclass
	class res_t:
		@dataclasses.dataclass
		class download_info_t:
			url: str
			sha256: str
			constraint: str

		txt: Optional[str] = None
		entries: Optional[list[download_info_t]] = None


def pip_resolve_entries_to_txt(entries: list[pip_resolve_t.res_t.download_info_t]) -> str:
	return '\n'.join(['#%s\n%s %s' % (o.url, o.constraint, ' '.join(['--hash=sha256:%s' % o2 for o2 in o.sha256])) for o in entries])


def pip_resolve(
	argv: list[str],
	mode: pip_resolve_t.kwargs_t.mode_t,
	requirements: Optional[list[str]] = None,
) -> pip_resolve_t.res_t:
	if mode is pip_resolve_t.kwargs_t.mode_t.copy_paste:
		import pip._internal.commands.show
		import pip._internal.commands.download
		import pip._internal.cli.cmdoptions
		import pip._internal.cli.main_parser
		import pip._internal.models.index
		import pip._internal.utils.temp_dir
		import pip._internal.cli.main
		import pip._internal.network.download
		import pip._internal.resolution.base
		import pip._internal.req.req_install
		import pip._internal.resolution.resolvelib.resolver
		import pip._internal.operations.prepare
		import pip._internal.utils.temp_dir
		import pip._internal.operations.build.build_tracker
		import pip._internal.models.direct_url

		with contextlib.ExitStack() as stack:
			stack.enter_context(pip._internal.utils.temp_dir.global_tempdir_manager())

			t2 = pip._internal.cli.main_parser.create_main_parser()

			t3 = t2.parse_args(['download'])
			t1 = pip._internal.commands.download.DownloadCommand('blah', 'shit')

			stack.enter_context(t1.main_context())

			# options = pip._internal.commands.download.Values()
			options = t3[0]
			options.python_version = None
			options.platforms = []
			options.abis = []
			options.implementation = []
			options.format_control = None
			options.ignore_dependencies = None
			options.index_url = pip._internal.models.index.PyPI.simple_url
			options.extra_index_urls = []
			options.no_index = None
			options.find_links = []
			options.pre = None
			options.prefer_binary = True
			options.only_binary = True
			options.constraints = []
			options.use_pep517 = None
			options.editables = []
			options.requirements = []
			options.src_dir = str(pathlib.Path(__file__).parent)
			options.build_isolation = None
			options.check_build_deps = None
			options.progress_bar = True
			options.require_hashes = None
			options.ignore_requires_python = False
			# options.cache_dir
			pip._internal.cli.cmdoptions.check_dist_restriction(options)
			# t1._in_main_context = True
			session = t1.get_default_session(options)
			target_python = pip._internal.cli.cmdoptions.make_target_python(options)
			finder = cast(pip_resolve_t.build_package_finder_t, getattr(t1, '_build_package_finder'))(
				options=options,
				session=session,
				target_python=target_python,
				ignore_requires_python=options.ignore_requires_python,
			)

			build_tracker = t1.enter_context(pip._internal.operations.build.build_tracker.get_build_tracker())
			reqs = t1.get_requirements(
				[
					#'pip', 'uv', 'ipython',
					*argv,
				],
				options,
				finder,
				session,
			)
			pip._internal.req.req_install.check_legacy_setup_py_options(options, reqs)
			directory = pip._internal.utils.temp_dir.TempDirectory(delete=True, kind='download', globally_managed=True)
			preparer = t1.make_requirement_preparer(
				temp_build_dir=directory,
				options=options,
				build_tracker=build_tracker,
				session=session,
				finder=finder,
				download_dir=None,
				use_user_site=False,
				verbosity=False,
			)
			resolver = t1.make_resolver(
				preparer=preparer,
				finder=finder,
				options=options,
				ignore_requires_python=options.ignore_requires_python,
				use_pep517=options.use_pep517,
				py_version_info=options.python_version,
			)
			t1.trace_basic_info(finder)
			requirement_set = resolver.resolve(reqs, check_supported_wheels=True)

			res = pip_resolve_t.res_t()

			res.entries = []

			for k, v in requirement_set.requirements.items():
				assert not v.download_info is None
				assert isinstance(
					v.download_info.info,
					pip._internal.models.direct_url.ArchiveInfo,
				)
				assert not v.download_info.info.hashes is None

				res.entries.append(
					pip_resolve_t.res_t.download_info_t(
						constraint=k,
						sha256=v.download_info.info.hashes['sha256'],
						url=v.download_info.url,
					)
				)

			res.txt = pip_resolve_entries_to_txt(res.entries)

			return res
	elif mode is pip_resolve_t.kwargs_t.mode_t.monkey_patch:
		import pip._internal.commands.show
		import pip._internal.commands.download
		import pip._internal.cli.main_parser
		import pip._internal.models.index
		import pip._internal.models.link
		from pip._internal.models.link import (
			Link,
		)
		import pip._internal.utils.temp_dir
		from pip._internal.metadata.base import (
			BaseDistribution,
		)
		import pip._internal.cli.main
		import pip._internal.network.download
		import pip._internal.resolution.base
		import pip._internal.resolution.resolvelib.resolver
		import pip._internal.operations.prepare
		from pip._internal.network.download import (
			Downloader,
		)
		from pip._internal.operations.prepare import (
			File,
		)
		from pip._internal.req.req_set import RequirementSet
		from pip._internal.utils.hashes import Hashes
		from pip._internal.req.req_install import InstallRequirement

		from . import cli_bootstrap

		downloader_call_def = pip._internal.network.download.Downloader.__call__

		def downloader_call(
			_self: pip._internal.network.download.Downloader,
			link: pip._internal.models.link.Link,
			location: str,
		) -> tuple[str, str]:
			logger.info(
				dict(
					url=link.url,
				)
			)

			return downloader_call_def(
				_self,
				link,
				location,
			)

		batch_downloader_call_def = pip._internal.network.download.BatchDownloader.__call__

		def batch_downloader_call(
			_self: pip._internal.network.download.BatchDownloader,
			links: Iterable[pip._internal.models.link.Link],
			location: str,
		) -> Iterable[tuple[pip._internal.models.link.Link, tuple[str, str]]]:
			# print(args)

			logger.info(
				dict(
					links=links,
					location=location,
				)
			)

			return [(o, ('/dev/null', '')) for o in links]

		# base_resolver_resolve_def = pip._internal.resolution.base.BaseResolver.resolve
		base_resolver_resolve_def = pip._internal.resolution.resolvelib.resolver.Resolver.resolve

		result_requirements: list[RequirementSet | InstallRequirement] = []

		def base_resolver_resolve(
			_self: pip._internal.resolution.resolvelib.resolver.Resolver,
			root_reqs: list[InstallRequirement,],
			check_supported_wheels: bool,
		) -> RequirementSet:
			# print(args, kwargs)

			res = base_resolver_resolve_def(_self, root_reqs, check_supported_wheels)

			result_requirements.append(res)
			raise NotImplementedError
			# return res

		get_http_url_def = pip._internal.operations.prepare.get_http_url

		def get_http_url(
			link: Link,
			download: Downloader,
			download_dir: Optional[str] = None,
			hashes: Optional[Hashes] = None,
		) -> File:
			logger.info(
				dict(
					url=link.url,
					hashes=hashes,
				)
			)

			if link.url.endswith('.whl'):
				print('blah')
				hashes = None

				return File(
					'/dev/null',
					'',
				)
			else:
				return get_http_url_def(link, download, download_dir, hashes)

		prepare_linked_requirements_more_def = pip._internal.operations.prepare.RequirementPreparer.prepare_linked_requirements_more

		def prepare_linked_requirements_more(
			_self: pip._internal.resolution.resolvelib.resolver.Resolver,
			reqs: Iterable[InstallRequirement],
			parallel_builds: bool = False,
		) -> None:
			result_requirements.extend(reqs)
			raise NotImplementedError

		_complete_partial_requirements_def = cast(
			pip_resolve_t.complete_partial_requirements_t,
			getattr(
				pip._internal.operations.prepare.RequirementPreparer,
				'_complete_partial_requirements',
			),
		)

		def _complete_partial_requirements(
			_self: pip._internal.resolution.resolvelib.resolver.Resolver,
			partially_downloaded_reqs: Iterable[InstallRequirement],
			parallel_builds: bool = False,
		) -> None:
			result_requirements.extend(partially_downloaded_reqs)
			raise NotImplementedError

		patches: list[Any] = []

		patches.append(unittest.mock.patch.object(pip._internal.network.download.Downloader, '__call__', downloader_call))
		# patches.append(
		#    unittest.mock.patch.object(
		#        pip._internal.network.download.BatchDownloader,
		#        '__call__',
		#        batch_downloader_call
		#    )
		# )
		# patches.append(
		#    unittest.mock.patch.object(
		#        pip._internal.resolution.base.BaseResolver, 'resolve', base_resolver_resolve))

		patches.append(
			unittest.mock.patch.object(
				pip._internal.resolution.resolvelib.resolver.Resolver,
				'resolve',
				base_resolver_resolve,
			)
		)
		patches.append(
			unittest.mock.patch.object(
				pip._internal.operations.prepare,
				'get_http_url',
				get_http_url,
			)
		)
		patches.append(
			unittest.mock.patch.object(
				pip._internal.operations.prepare.RequirementPreparer,
				'prepare_linked_requirements_more',
				prepare_linked_requirements_more,
			)
		)
		# patches.append(
		#    unittest.mock.patch.object(
		#        pip._internal.operations.prepare.RequirementPreparer,
		#        '_complete_partial_requirements',
		#        _complete_partial_requirements
		#    )
		# )

		with contextlib.ExitStack() as stack:
			for p in patches:
				stack.enter_context(p)

			pip._internal.cli.main.main(
				[
					'download',
					'-q',
					'--no-cache',
					'-d',
					'/dev/null',
					*argv,
					# 'numpy',
				]
			)

		# return sum([
		#    [
		#        pip_resolve_t.res_t.download_info_t(
		#            constraint=k,
		#            sha256=v.download_info.info.hashes['sha256'],
		#            url=v.download_info.url,
		#        )
		#        for k, v in o.requirements.items()
		#    ]
		#    for o in result_requirements
		# ], [])
		logger.warning(result_requirements)

		res = pip_resolve_t.res_t()

		res.entries = []

		for o in result_requirements:
			assert isinstance(o, InstallRequirement)

			sha256_hashes = cli_bootstrap.check_list(
				cli_bootstrap.check_dict(
					getattr(o.hashes(), '_allowed'),
					str,
					list[str],
				)['sha256'],
				str,
			)

			assert len(sha256_hashes) == 1
			assert not o.link is None

			res.entries.append(
				pip_resolve_t.res_t.download_info_t(
					constraint=str(o.req),
					sha256=sha256_hashes[0],
					url=o.link.url,
				)
			)

		res.txt = pip_resolve_entries_to_txt(res.entries)

		return res
	elif mode is pip_resolve_t.kwargs_t.mode_t.uv_pip_freeze:
		assert len(argv) == 0

		pip_freeze = subprocess.check_output(
			[
				sys.executable,
				'-m',
				'uv',
				'pip',
				'freeze',
			],
		).decode('utf-8')
		pip_compile = subprocess.check_output(
			[
				sys.executable,
				'-m',
				'uv',
				'pip',
				'compile',
				'--generate-hashes',
				'-',
			],
			input=pip_freeze.encode('utf-8'),
		).decode('utf-8')

		return pip_resolve_t.res_t(
			txt=pip_compile,
		)
	elif mode is pip_resolve_t.kwargs_t.mode_t.uv_pip_compile:
		with contextlib.ExitStack() as stack:
			if not requirements is None:
				# assert len(argv) == 0

				f = stack.enter_context(
					tempfile.NamedTemporaryFile(
						mode='w',
						suffix='.txt',
					)
				)
				f.write('\n'.join(requirements))
				f.flush()

				argv.append(f.name)

			if argv[0] == '--':
				del argv[0]

			pip_compile = subprocess.check_output(
				[
					sys.executable,
					'-m',
					'uv',
					'pip',
					'compile',
					'--generate-hashes',
					*argv,
				],
			).decode('utf-8')

			return pip_resolve_t.res_t(
				txt=pip_compile,
			)
	else:
		raise NotImplementedError


class pip_check_conflicts_t:
	@dataclasses.dataclass
	class res_t:
		status: Literal['ok', 'error']
		duplicates: list[str]


def pip_check_conflicts(
	venv_path: Optional[pathlib.Path] = None,
) -> pip_check_conflicts_t.res_t:
	assert sys.platform == 'linux'
	if venv_path is None:
		venv_path = (
			pathlib.Path(
				sys.executable,
			).parent
			/ '..'
		)

	# records = glob.glob(
	#     str(venv_path / '*' / 'site-packages' / '*.dist-info' / 'RECORD'),
	#     recursive=True,
	# )

	duplicates = [
		line
		for line in subprocess.check_output(
			r"""
            cat $(find $VENV_PATH/lib/*/*/*.dist-info/RECORD)  | sort | uniq -c  | (grep -v -P '^\s+1\s'; true;)
        """,
			shell=True,
			env=dict(
				VENV_PATH=str(venv_path),
			),
		)
		.decode('utf-8')
		.splitlines()
		if line.strip() != ''
	]

	return pip_check_conflicts_t.res_t(status=('error' if len(duplicates) > 0 else 'ok'), duplicates=duplicates)
