"""
Per-project git config UI backed by config-options.json (all sections except color and alias).
"""

from __future__ import annotations

import json
from contextlib import suppress
from pathlib import Path
from typing import Any, Final
from urllib.parse import quote

import pygit2
from fastapi import HTTPException
from fastapi.responses import HTMLResponse, JSONResponse

from pygitweb import config
from pygitweb.config import EXPORT_OK, PROJECTROOT, STRICT_EXPORT
from pygitweb.formatting import esc_html
from pygitweb.git_helpers import open_repo
from pygitweb.projects import git_get_projects_list
from pygitweb.settings import GIT_CONFIG_LEVEL_LOCAL, GIT_CONFIG_LEVEL_WORKTREE
from pygitweb.templates_env import POSTAMBLE, PREAMBLE, env
from pygitweb.validation import is_valid_project

_GIT_CONFIG_OPTIONS_PATH: Final[Path] = Path(__file__).resolve().parent / "data" / "config-options.json"

_EXCLUDED_CONFIG_SECTIONS: Final[frozenset[str]] = frozenset({"color", "alias"})

_ORDERED_GIT_CONFIG_OPTIONS: list[dict[str, Any]] = []
_OPTION_BY_KEY: dict[str, dict[str, Any]] = {}


def init_git_config_options_registry() -> None:
	"""Load config-options.json and index allowed options (startup). Skips [color], [alias], and subsection '*'."""
	global _ORDERED_GIT_CONFIG_OPTIONS, _OPTION_BY_KEY
	raw = json.loads(_GIT_CONFIG_OPTIONS_PATH.read_text(encoding="utf-8"))
	if not isinstance(raw, list):
		raise RuntimeError("config-options.json must be a JSON array")
	by_key: dict[str, dict[str, Any]] = {}
	ordered: list[dict[str, Any]] = []
	for item in raw:
		if not isinstance(item, dict):
			continue
		sec = item.get("section")
		if not isinstance(sec, str) or sec in _EXCLUDED_CONFIG_SECTIONS:
			continue
		if item.get("subsection") == "*":
			continue
		try:
			k = _git_config_key(item)
		except (KeyError, TypeError, ValueError):
			continue
		if k in by_key:
			continue
		by_key[k] = item
		ordered.append(item)
	_ORDERED_GIT_CONFIG_OPTIONS = ordered
	_OPTION_BY_KEY = by_key


def _git_config_key(item: dict[str, Any]) -> str:
	section = str(item["section"])
	subsection = item.get("subsection")
	leaf = item.get("configName") or item.get("name")
	if not isinstance(leaf, str) or not leaf:
		raise ValueError("invalid config option entry: missing name")
	if subsection and subsection != "*":
		return f"{section}.{subsection}.{leaf}"
	return f"{section}.{leaf}"


def _project_in_list(project: str) -> bool:
	lst = git_get_projects_list(
		filter_path="",
		paranoid=STRICT_EXPORT,
		export_ok=EXPORT_OK,
	)
	return any(p.get("path") == project for p in lst)


def _validate_project_or_404(project: str | None) -> str:
	if not project:
		raise HTTPException(status_code=400, detail="Project needed")
	if not is_valid_project(
		project,
		PROJECTROOT,
		EXPORT_OK,
		STRICT_EXPORT,
		_project_in_list,
	):
		raise HTTPException(status_code=404, detail="No such project")
	return project


def _is_option_type(t: str) -> bool:
	return t == "select"


def _is_textual_type(t: str) -> bool:
	return t in ("text", "email") or not _is_option_type(t)


def _repo_config_local_worktree_values(repo: pygit2.Repository) -> dict[str, str]:
	"""
	Config values from LOCAL (5) and WORKTREE (6) only; last value wins per name.
	Same iteration rules as settings._get_project_config_entries.
	"""
	cfg = repo.config
	by_name: dict[str, str] = {}
	for entry in cfg:
		if entry.level not in (GIT_CONFIG_LEVEL_LOCAL, GIT_CONFIG_LEVEL_WORKTREE):
			continue
		by_name[entry.name] = entry.value
	return by_name


def _field_dict(item: dict[str, Any], local_worktree: dict[str, str]) -> dict[str, Any]:
	key = _git_config_key(item)
	current = local_worktree.get(key, "")
	opt_type = str(item.get("type") or "text")
	return {
		"git_key": key,
		"display_name": str(item.get("displayName") or item.get("name") or key),
		"description": str(item.get("description") or ""),
		"control_kind": "select" if _is_option_type(opt_type) else "text",
		"current": current,
		"options": list(item["options"]) if isinstance(item.get("options"), list) else [],
	}


def _group_options_by_section_and_subsection(
	items: list[dict[str, Any]],
) -> list[dict[str, Any]]:
	"""Same grouping as gitconfig-generator: section → subsection → options (order preserved)."""
	section_order: list[str] = []
	section_subsection_order: dict[str, list[str]] = {}
	buckets: dict[str, dict[str, list[dict[str, Any]]]] = {}
	for item in items:
		sec = str(item.get("section") or "")
		raw_sub = item.get("subsection")
		sub = "" if raw_sub in (None, "") else str(raw_sub)
		if sub == "*":
			continue
		if sec not in buckets:
			buckets[sec] = {}
			section_order.append(sec)
			section_subsection_order[sec] = []
		if sub not in buckets[sec]:
			buckets[sec][sub] = []
			section_subsection_order[sec].append(sub)
		buckets[sec][sub].append(item)
	out: list[dict[str, Any]] = []
	for sec in section_order:
		subs: list[dict[str, Any]] = []
		for sub in section_subsection_order[sec]:
			subs.append({"subsection": sub, "items": buckets[sec][sub]})
		out.append({"section": sec, "subsections": subs})
	return out


def project_git_config_page(project: str) -> HTMLResponse:
	"""HTML page listing git config options;
	Values shown are LOCAL + WORKTREE only (see settings._get_project_config_entries)."""
	project = _validate_project_or_404(project)
	if not _OPTION_BY_KEY:
		raise HTTPException(status_code=500, detail="Git config registry not loaded")
	repo = open_repo(project)
	local_worktree = _repo_config_local_worktree_values(repo)
	grouped = _group_options_by_section_and_subsection(_ORDERED_GIT_CONFIG_OPTIONS)
	sections: list[dict[str, Any]] = []
	for sec in grouped:
		subsections_out: list[dict[str, Any]] = []
		for sub in sec["subsections"]:
			fields = [_field_dict(item, local_worktree) for item in sub["items"]]
			subsections_out.append({"subsection": sub["subsection"], "fields": fields})
		sections.append({"section": sec["section"], "subsections": subsections_out})
	pre = PREAMBLE.render(
		title=f"{esc_html(config.SITE_NAME)} - Git config — {esc_html(project)}",
		site_name=config.SITE_NAME,
	)
	project_qp = quote(project, safe="/")
	body = env.get_template("project_git_config.html").render(
		project=project,
		project_esc=esc_html(project),
		summary_url=f"/project/{project_qp}",
		set_config_url=f"/project/{project_qp}?a=set_config",
		sections=sections,
	)
	return HTMLResponse(f"{pre}{body}{POSTAMBLE}")


def _normalize_set_value(opt: dict[str, Any], raw: str) -> str:
	opt_type = str(opt.get("type") or "text")
	stripped = raw.strip()
	if not stripped:
		return ""
	if _is_option_type(opt_type):
		options = opt.get("options")
		if not isinstance(options, list) or stripped not in [str(x) for x in options]:
			raise HTTPException(
				status_code=400,
				detail="value must be one of the allowed options for this key",
			)
		return stripped
	if not _is_textual_type(opt_type):
		raise HTTPException(status_code=400, detail="unsupported config type for this key")
	return raw


def project_git_config_set_post(project: str, key: str, value: str | None = None) -> JSONResponse:
	"""Set or unset a single local config key validated against config-options.json."""
	project = _validate_project_or_404(project)
	if not _OPTION_BY_KEY:
		raise HTTPException(status_code=500, detail="Git config registry not loaded")
	lookup = (key or "").strip()
	if not lookup:
		raise HTTPException(status_code=400, detail="key is required")
	opt = _OPTION_BY_KEY.get(lookup)
	if opt is None:
		raise HTTPException(status_code=400, detail="unknown or unsupported config key")
	raw_val = value if value is not None else ""
	normalized = _normalize_set_value(opt, raw_val)
	repo = open_repo(project)
	cfg = repo.config
	try:
		if not normalized:
			with suppress(KeyError):
				del cfg[lookup]
		else:
			cfg[lookup] = normalized
	except (pygit2.GitError, TypeError, ValueError) as e:
		raise HTTPException(status_code=400, detail=f"could not update config: {e}") from e
	return JSONResponse({"ok": True})