"""Load pgt terminal colors from the user's Git configuration."""

from __future__ import annotations

import json
import math
from dataclasses import dataclass
from importlib.resources import files
from typing import TYPE_CHECKING

from pygit2 import Config

from pygittools.tui.ucurses import (  # type: ignore[import-untyped]
	A_BOLD,
	A_DIM,
	A_REVERSE,
	A_UNDERLINE,
	COLOR_BLACK,
	COLOR_BLUE,
	COLOR_CYAN,
	COLOR_GREEN,
	COLOR_PAIR,
	COLOR_RED,
	COLOR_WHITE,
	COLOR_YELLOW,
	can_change_color,
	has_colors,
	init_color,
	init_pair,
	start_color,
	use_default_colors,
)

if TYPE_CHECKING:
	from pygit2 import Repository

_PAIR_STATUS = 1
_PAIR_TAB_ACTIVE = 2
_PAIR_STAGED = 3
_PAIR_UNSTAGED = 4
_PAIR_UNTRACKED = 5
_PAIR_HEADER = 6
_PAIR_BRANCH = 7
_PAIR_SECTION_HEADER = 8
_PAIR_LOG_HASH = 9
_PAIR_LOG_DATE = 10
_PAIR_LOG_SUBJECT = 11

_NAMED_RGB: dict[str, tuple[int, int, int]] = {
	"normal": (204, 204, 204),
	"black": (0, 0, 0),
	"red": (170, 0, 0),
	"green": (0, 170, 0),
	"yellow": (170, 170, 0),
	"blue": (0, 0, 170),
	"magenta": (170, 0, 170),
	"cyan": (0, 170, 170),
	"white": (204, 204, 204),
}

_BRIGHT_NAMED_RGB: dict[str, tuple[int, int, int]] = {
	"black": (85, 85, 85),
	"red": (255, 0, 0),
	"green": (0, 255, 0),
	"yellow": (255, 255, 0),
	"blue": (0, 0, 255),
	"magenta": (255, 0, 255),
	"cyan": (0, 255, 255),
	"white": (255, 255, 255),
}

_CURSES_RGB: tuple[tuple[int, int, int], ...] = (
	(0, 0, 0),
	(128, 0, 0),
	(0, 128, 0),
	(128, 128, 0),
	(0, 0, 128),
	(128, 0, 128),
	(0, 128, 128),
	(192, 192, 192),
)

_DEFAULT_SPECS: dict[str, str] = {
	"color.status.added": "green",
	"color.status.changed": "red",
	"color.status.untracked": "red",
	"color.status.header": "normal",
	"color.status.branch": "cyan",
	"color.status.nobranch": "red bold",
	"color.branch.current": "green bold",
	"color.branch.local": "cyan",
	"color.diff.new": "green",
	"color.diff.old": "red",
	"color.diff.context": "normal",
	"color.decorate.commit": "yellow",
}

# Light grey section dropdown labels; file rows keep color.status.* from Git.
_DEFAULT_SECTION_HEADER_SPEC = "252"

# Log date column default when Git has no matching slot.
_DEFAULT_LOG_DATE_SPEC = "246"

_FALSE_VALUES = frozenset({"0", "false", "never", "no", "off"})


@dataclass(frozen=True, slots=True)
class ParsedGitColor:
	foreground: tuple[int, int, int] | None
	background: tuple[int, int, int] | None
	bold: bool = False
	dim: bool = False
	reverse: bool = False
	underline: bool = False


@dataclass(frozen=True, slots=True)
class TuiColorTheme:
	enabled: bool
	status_bar: int
	tab_active: int
	tab_inactive: int
	focus: int
	staged: int
	unstaged: int
	untracked: int
	section_header: int
	header: int
	branch: int
	commit: int
	log_hash: int
	log_date: int
	log_subject: int
	normal: int


_theme = TuiColorTheme(
	enabled=False,
	status_bar=A_REVERSE,
	tab_active=A_BOLD,
	tab_inactive=0,
	focus=A_REVERSE,
	staged=0,
	unstaged=0,
	untracked=0,
	section_header=0,
	header=A_BOLD,
	branch=0,
	commit=0,
	log_hash=0,
	log_date=0,
	log_subject=0,
	normal=0,
)


def current_theme() -> TuiColorTheme:
	return _theme


def status_bar_attr() -> int:
	return _theme.status_bar


def active_tab_attr() -> int:
	return _theme.tab_active


def init_tui_color_theme(repo: Repository | None) -> TuiColorTheme:
	"""Read Git color settings and initialize curses color pairs."""
	global _theme
	config = _load_config(repo)
	if not _colors_enabled(config):
		_theme = _monochrome_theme()
		return _theme

	palette = _load_palette()
	specs = _load_color_specs(config)
	if not has_colors():
		_theme = _attr_only_theme(specs, palette)
		return _theme

	start_color()
	use_default_colors()
	slots = _ColorSlotAllocator()
	header_attr = _build_pair_attr(
		specs.get("color.status.header", _DEFAULT_SPECS["color.status.header"]),
		palette,
		slots,
		_PAIR_HEADER,
		fallback_fg=COLOR_WHITE,
		extra_attr=A_BOLD,
	)
	_theme = TuiColorTheme(
		enabled=True,
		status_bar=_build_pair_attr(
			specs.get("color.status.header", _DEFAULT_SPECS["color.status.header"]),
			palette,
			slots,
			_PAIR_STATUS,
			fallback_fg=COLOR_WHITE,
			fallback_bg=COLOR_BLUE,
			background_rgb=(0, 0, 128),
		),
		tab_active=_build_pair_attr(
			specs.get("color.branch.current", _DEFAULT_SPECS["color.branch.current"]),
			palette,
			slots,
			_PAIR_TAB_ACTIVE,
			fallback_fg=COLOR_BLACK,
			fallback_bg=COLOR_YELLOW,
		),
		tab_inactive=0,
		focus=A_REVERSE,
		staged=_build_pair_attr(
			specs.get("color.status.added", _DEFAULT_SPECS["color.status.added"]),
			palette,
			slots,
			_PAIR_STAGED,
			fallback_fg=COLOR_GREEN,
		),
		unstaged=_build_pair_attr(
			specs.get("color.status.changed", _DEFAULT_SPECS["color.status.changed"]),
			palette,
			slots,
			_PAIR_UNSTAGED,
			fallback_fg=COLOR_RED,
		),
		untracked=_build_pair_attr(
			specs.get("color.status.untracked", _DEFAULT_SPECS["color.status.untracked"]),
			palette,
			slots,
			_PAIR_UNTRACKED,
			fallback_fg=COLOR_RED,
		),
		section_header=_build_pair_attr(
			_DEFAULT_SECTION_HEADER_SPEC,
			palette,
			slots,
			_PAIR_SECTION_HEADER,
			fallback_fg=COLOR_WHITE,
		),
		header=header_attr,
		branch=_build_pair_attr(
			specs.get("color.status.branch", _DEFAULT_SPECS["color.status.branch"]),
			palette,
			slots,
			_PAIR_BRANCH,
			fallback_fg=COLOR_CYAN,
		),
		commit=header_attr,
		log_hash=_build_pair_attr(
			specs.get("color.decorate.commit", _DEFAULT_SPECS["color.decorate.commit"]),
			palette,
			slots,
			_PAIR_LOG_HASH,
			fallback_fg=COLOR_YELLOW,
		),
		log_date=_build_pair_attr(
			_DEFAULT_LOG_DATE_SPEC,
			palette,
			slots,
			_PAIR_LOG_DATE,
			fallback_fg=COLOR_WHITE,
		),
		log_subject=_build_pair_attr(
			specs.get("color.diff.context", _DEFAULT_SPECS["color.diff.context"]),
			palette,
			slots,
			_PAIR_LOG_SUBJECT,
			fallback_fg=COLOR_WHITE,
		),
		normal=0,
	)
	return _theme


def parse_git_color_spec(spec: str | None) -> ParsedGitColor:
	if spec is None or not spec.strip():
		return ParsedGitColor(foreground=None, background=None)
	foreground: tuple[int, int, int] | None = None
	background: tuple[int, int, int] | None = None
	bold = False
	dim = False
	reverse = False
	underline = False
	palette = _load_palette()
	for raw_token in spec.strip().split():
		token = raw_token.casefold()
		if token.startswith("no-"):
			token = token[3:]
		if token in {"bold", "standout"}:
			bold = not raw_token.casefold().startswith("no")
			continue
		if token == "dim":
			dim = not raw_token.casefold().startswith("no")
			continue
		if token == "reverse":
			reverse = not raw_token.casefold().startswith("no")
			continue
		if token in {"ul", "underline"}:
			underline = not raw_token.casefold().startswith("no")
			continue
		if token in {"italic", "blink", "strike"}:
			continue
		rgb = _token_to_rgb(raw_token, palette)
		if rgb is None:
			continue
		if foreground is None:
			foreground = rgb
		elif background is None:
			background = rgb
	return ParsedGitColor(
		foreground=foreground,
		background=background,
		bold=bold,
		dim=dim,
		reverse=reverse,
		underline=underline,
	)


def _load_config(repo: Repository | None) -> Config:
	if repo is not None:
		return repo.config.snapshot()
	return Config.get_global_config()


def _config_value(config: Config, key: str) -> str | None:
	try:
		values = list(config.get_multivar(key))
	except KeyError:
		return None
	if not values:
		return None
	return values[-1].strip()


def _colors_enabled(config: Config) -> bool:
	ui = _config_value(config, "color.ui")
	if ui is not None and ui.casefold() in _FALSE_VALUES:
		return False
	status = _config_value(config, "color.status")
	return status is None or status.casefold() not in _FALSE_VALUES


def _load_color_specs(config: Config) -> dict[str, str]:
	specs = dict(_DEFAULT_SPECS)
	for key in _DEFAULT_SPECS:
		value = _config_value(config, key)
		if value:
			specs[key] = value
	return specs


def _load_palette() -> tuple[tuple[int, int, int], ...]:
	raw = json.loads(files("pygittools.tui").joinpath("colors.json").read_text(encoding="utf-8"))
	palette: list[tuple[int, int, int]] = []
	for entry in raw:
		text = str(entry).strip()
		if not text.startswith("#") or len(text) not in {4, 7}:
			palette.append((0, 0, 0))
			continue
		if len(text) == 4:
			text = f"#{text[1]}{text[1]}{text[2]}{text[2]}{text[3]}{text[3]}"
		palette.append((int(text[1:3], 16), int(text[3:5], 16), int(text[5:7], 16)))
	return tuple(palette)


def _token_to_rgb(token: str, palette: tuple[tuple[int, int, int], ...]) -> tuple[int, int, int] | None:
	lowered = token.casefold()
	if lowered == "default":
		return None
	if token.startswith("#"):
		text = token
		if len(text) == 4:
			text = f"#{text[1]}{text[1]}{text[2]}{text[2]}{text[3]}{text[3]}"
		if len(text) == 7:
			return (int(text[1:3], 16), int(text[3:5], 16), int(text[5:7], 16))
		return None
	if token.isdecimal():
		index = int(token)
		if 0 <= index < len(palette):
			return palette[index]
		return None
	bright = False
	name = lowered
	if name.startswith("bright"):
		bright = True
		name = name[6:]
	if name in _NAMED_RGB:
		if bright and name in _BRIGHT_NAMED_RGB:
			return _BRIGHT_NAMED_RGB[name]
		red, green, blue = _NAMED_RGB[name]
		return (red, green, blue)
	return None


class _ColorSlotAllocator:
	def __init__(self) -> None:
		self._next_custom = 1
		self._rgb_by_slot: dict[int, tuple[int, int, int]] = {}

	def slot_for(self, rgb: tuple[int, int, int]) -> int:
		for slot, existing in self._rgb_by_slot.items():
			if existing == rgb:
				return slot
		if can_change_color() and self._next_custom <= 6:
			slot = self._next_custom
			self._next_custom += 1
			self._rgb_by_slot[slot] = rgb
			init_color(slot, _curses_component(rgb[0]), _curses_component(rgb[1]), _curses_component(rgb[2]))
			return slot
		return _nearest_curses_index(rgb)


def _build_pair_attr(
	spec: str,
	palette: tuple[tuple[int, int, int], ...],
	slots: _ColorSlotAllocator,
	pair_id: int,
	*,
	fallback_fg: int,
	fallback_bg: int = COLOR_BLACK,
	background_rgb: tuple[int, int, int] | None = None,
	extra_attr: int = 0,
) -> int:
	parsed = parse_git_color_spec(spec)
	fg_rgb = parsed.foreground
	bg_rgb = parsed.background or background_rgb
	fg = fallback_fg if fg_rgb is None else slots.slot_for(fg_rgb)
	bg = fallback_bg if bg_rgb is None else slots.slot_for(bg_rgb)
	init_pair(pair_id, fg, bg)
	attr = COLOR_PAIR(pair_id)
	if parsed.bold:
		attr |= A_BOLD
	if parsed.dim:
		attr |= A_DIM
	if parsed.underline:
		attr |= A_UNDERLINE
	if parsed.reverse:
		attr |= A_REVERSE
	return attr | extra_attr


def _monochrome_theme() -> TuiColorTheme:
	return TuiColorTheme(
		enabled=False,
		status_bar=A_REVERSE,
		tab_active=A_BOLD,
		tab_inactive=0,
		focus=A_REVERSE,
		staged=0,
		unstaged=0,
		untracked=0,
		section_header=0,
		header=A_BOLD,
		branch=0,
		commit=0,
		log_hash=0,
		log_date=0,
		log_subject=0,
		normal=0,
	)


def _attr_only_theme(specs: dict[str, str], palette: tuple[tuple[int, int, int], ...]) -> TuiColorTheme:
	def attrs(key: str, *, extra: int = 0) -> int:
		parsed = parse_git_color_spec(specs.get(key, _DEFAULT_SPECS[key]))
		value = extra
		if parsed.bold:
			value |= A_BOLD
		if parsed.dim:
			value |= A_DIM
		if parsed.underline:
			value |= A_UNDERLINE
		if parsed.reverse:
			value |= A_REVERSE
		return value

	return TuiColorTheme(
		enabled=False,
		status_bar=attrs("color.status.header") or A_REVERSE,
		tab_active=attrs("color.branch.current", extra=A_BOLD) or A_BOLD,
		tab_inactive=0,
		focus=A_REVERSE,
		staged=attrs("color.status.added"),
		unstaged=attrs("color.status.changed"),
		untracked=attrs("color.status.untracked"),
		section_header=_spec_attrs(_DEFAULT_SECTION_HEADER_SPEC),
		header=attrs("color.status.header", extra=A_BOLD) or A_BOLD,
		branch=attrs("color.status.branch"),
		commit=attrs("color.diff.new"),
		log_hash=attrs("color.decorate.commit"),
		log_date=_spec_attrs(_DEFAULT_LOG_DATE_SPEC),
		log_subject=attrs("color.diff.context"),
		normal=0,
	)


def _spec_attrs(spec: str, *, extra: int = 0) -> int:
	parsed = parse_git_color_spec(spec)
	value = extra
	if parsed.bold:
		value |= A_BOLD
	if parsed.dim:
		value |= A_DIM
	if parsed.underline:
		value |= A_UNDERLINE
	if parsed.reverse:
		value |= A_REVERSE
	return value


def _nearest_curses_index(rgb: tuple[int, int, int]) -> int:
	best_index = 0
	best_distance = math.inf
	for index, candidate in enumerate(_CURSES_RGB):
		distance = _rgb_distance(rgb, candidate)
		if distance < best_distance:
			best_distance = distance
			best_index = index
	return best_index


def _rgb_distance(left: tuple[int, int, int], right: tuple[int, int, int]) -> float:
	return math.sqrt(sum((a - b) ** 2 for a, b in zip(left, right, strict=True)))


def _curses_component(value: int) -> int:
	return max(0, min(1000, round(value * 1000 / 255)))


def focus_attr(base: int) -> int:
	if base & A_REVERSE:
		return base
	return base | A_REVERSE


def row_attr_for_section(
	section: str | None,
	*,
	focused: bool,
	highlight: bool,
	is_header: bool = False,
) -> int:
	theme = _theme
	if is_header:
		base = theme.section_header
	elif section == "staged":
		base = theme.staged
	elif section == "unstaged":
		base = theme.unstaged
	elif section == "untracked":
		base = theme.untracked
	else:
		base = theme.normal
	if focused and highlight:
		return focus_attr(base)
	return base


def log_entry_attrs(*, focused: bool, highlight: bool) -> tuple[int, int, int]:
	theme = _theme
	if focused and highlight:
		return (
			focus_attr(theme.log_hash),
			focus_attr(theme.log_date),
			focus_attr(theme.log_subject),
		)
	return (theme.log_hash, theme.log_date, theme.log_subject)