import pydantic.dataclasses
import datetime
import pydantic_settings
import marisa_trie
import json
import pathlib
import subprocess
import logging
import sys
import argparse

from pydantic import (
	Field,
)

from typing import (
	ClassVar,
	Generator,
	Annotated,
	Optional,
	Any,
)


logger = logging.getLogger(__name__)


@pydantic.dataclasses.dataclass
class MypyFormatEntry:
	name: str
	value: str

	def __eq__(self, other: object) -> bool:
		if not isinstance(other, type(self)):
			raise NotImplementedError

		return self.value == other.value


class MypyFormat:
	vscode: ClassVar[MypyFormatEntry] = MypyFormatEntry(name='vscode', value='vscode')
	json: ClassVar[MypyFormatEntry] = MypyFormatEntry(name='json', value='json')

	@classmethod
	def from_value(cls, value: str) -> MypyFormatEntry:
		for e in cls.entries():
			if value == e.value:
				return e

		raise NotImplementedError

	@classmethod
	def entries(
		cls,
	) -> Generator[
		MypyFormatEntry,
		None,
		None,
	]:
		for o in dir(cls):
			e = getattr(cls, o)
			if not isinstance(e, MypyFormatEntry):
				continue

			yield e


class MypySettings(pydantic_settings.BaseSettings):
	model_config = pydantic_settings.SettingsConfigDict(
		env_prefix='online_fxreader_pr34_mypy_',
		case_sensitive=False,
	)

	config_path: pathlib.Path = pathlib.Path.cwd() / '.mypy.ini'
	max_errors: dict[str, int] = dict()
	paths: Annotated[list[pathlib.Path], Field(default_factory=lambda: ['.'])]


def run(
	argv: Optional[list[str]] = None,
	settings: Optional[MypySettings] = None,
) -> None:
	if argv is None:
		argv = []

	if settings is None:
		settings = MypySettings.model_validate(dict())

	parser = argparse.ArgumentParser()
	parser.add_argument(
		'-q',
		'--quiet',
		dest='quiet',
		action='store_true',
		help='do not print anything if the program is correct according to max_errors limits',
		default=False,
	)
	parser.add_argument(
		'-i',
		dest='paths',
		help='specify paths to check',
		default=[],
		action='append',
	)
	parser.add_argument(
		'-f',
		'--format',
		dest='_format',
		help='output format of errors',
		default=MypyFormat.json.value,
		choices=[o.value for o in MypyFormat.entries()],
	)
	options, args = parser.parse_known_args(argv)

	if len(args) > 0 and args[0] == '--':
		del args[0]

	options.format = MypyFormat.from_value(options._format)

	if len(options.paths) == 0:
		options.paths.extend(settings.paths)

	started_at = datetime.datetime.now()

	mypy_cmd = [
		sys.executable,
		'-m',
		'mypy',
		'--config-file',
		str(settings.config_path),
		'--strict',
		'-O',
		'json',
		*args,
		*options.paths,
	]

	logger.info(dict(cmd=mypy_cmd))

	res = subprocess.run(
		mypy_cmd,
		stdout=subprocess.PIPE,
		stderr=subprocess.PIPE,
	)

	done_at = datetime.datetime.now()

	try:
		assert not res.returncode is None

		errors = sorted(
			[json.loads(o) for o in res.stdout.decode('utf-8').splitlines() if not o.strip() == ''],
			key=lambda x: (
				x.get('file', ''),
				x.get('line', 0),
			),
		)

		if not options.quiet:
			if (len(res.stderr)) > 0:
				logger.error(res.stderr.decode('utf-8'))
	except:
		logger.exception('')
		logger.error(res.stdout.decode('utf-8'))
		logger.error(res.stderr.decode('utf-8'))
		sys.exit(res.returncode)

	g: dict[str, Any] = dict()
	for o in errors:
		if not o['file'] in g:
			g[o['file']] = []
		g[o['file']].append(o)

	h = {
		k: len(v)
		for k, v in sorted(
			list(g.items()),
			key=lambda x: x[0],
		)
	}

	mentioned_paths = marisa_trie.Trie(list(h))

	violated_limits: dict[str, str] = dict()

	for k, v in settings.max_errors.items():
		matching_paths = mentioned_paths.keys(k)
		total_errors = sum([h[o] for o in matching_paths], 0)

		if total_errors > v:
			violated_limits[k] = '%s - [%s]: has %d errors > %d' % (
				k,
				', '.join(matching_paths),
				total_errors,
				v,
			)

	if len(violated_limits) > 0 or not options.quiet:
		if options.format == MypyFormat.vscode:
			for o in errors:
				sys.stdout.write(
					'[%s] %s:%d,%d %s - %s - %s\n'
					% (
						o['severity'],
						o['file'],
						o['line'],
						o['column'],
						o['message'],
						o['hint'],
						o['code'],
					)
				)
				sys.stdout.flush()
			# logger.info(json.dumps(errors, indent=4))
		else:
			logger.info(json.dumps(errors, indent=4))

		# if len(violated_limits) > 0:
		#    logger.info(json.dumps(violated_limits, indent=4))
		logger.info(
			json.dumps(
				dict(
					max_errors=settings.max_errors,
					violated_limits=violated_limits,
					histogram=h,
					elapsed=(done_at - started_at).total_seconds(),
				),
				indent=4,
			)
		)

	if len(violated_limits) > 0:
		sys.exit(1)


if __name__ == '__main__':
	from . import logging as _logging

	_logging.setup()
	run(sys.argv[1:])
