"""Unified PAT resolution and storage for all supported forge types."""

from __future__ import annotations

import configparser
import json
import logging
import os
from dataclasses import dataclass
from pathlib import Path

log = logging.getLogger(__name__)

AUTH_FILE = Path.home() / ".config" / "project-hub" / "auth.json"
PYTHON_GITLAB_CFG = Path.home() / ".python-gitlab.cfg"

_KEYRING_SERVICE = "project-hub"
_KEYRING_SENTINEL = "<keyring>"


@dataclass
class AuthInfo:
    """Authentication status for a single forge instance."""
    instance: str
    has_token: bool
    authenticated: bool
    backend: str = "file"


# ---------------------------------------------------------------------------
# Keyring helpers
# ---------------------------------------------------------------------------

def _keyring_available() -> bool:
    """Return True if OS keyring backend is functional."""
    try:
        import keyring
        from keyring.backends.fail import Keyring as FailKeyring
        return not isinstance(keyring.get_keyring(), FailKeyring)
    except Exception:
        return False


def _resolve_from_keyring(instance: str) -> str | None:
    """Try OS keyring (GNOME Keyring / macOS Keychain / Windows Credential Locker)."""
    try:
        import keyring
        token = keyring.get_password(_KEYRING_SERVICE, instance)
        return token or None
    except Exception:
        return None


def _store_to_keyring(instance: str, token: str) -> bool:
    """Store token in OS keyring. Returns True on success."""
    try:
        import keyring
        keyring.set_password(_KEYRING_SERVICE, instance, token)
        return True
    except Exception:
        return False


# ---------------------------------------------------------------------------
# File helpers
# ---------------------------------------------------------------------------

def _ensure_permissions() -> None:
    """Restrict auth.json to owner-only if permissions are too open."""
    if not AUTH_FILE.exists():
        return
    if AUTH_FILE.stat().st_mode & 0o077:
        os.chmod(str(AUTH_FILE), 0o600)
        log.info("auth.json permissions have been restricted to 0600")


def _resolve_from_auth_file(instance: str) -> str | None:
    """Try AUTH_FILE (~/.config/project-hub/auth.json)."""
    if not AUTH_FILE.exists():
        return None
    _ensure_permissions()
    try:
        data = json.loads(AUTH_FILE.read_text())
        value = data.get(instance)
        if value == _KEYRING_SENTINEL:
            return None
        return value
    except (json.JSONDecodeError, OSError):
        return None


def _resolve_from_gitlab_env(instance: str) -> str | None:
    """Try GITLAB_TOKEN env var (gitlab.com only)."""
    if instance != "gitlab.com":
        return None
    return os.environ.get("GITLAB_TOKEN") or None


def _resolve_from_github_env(instance: str) -> str | None:  # pylint: disable=unused-argument
    """Try GITHUB_TOKEN env var (any GitHub instance)."""
    return os.environ.get("GITHUB_TOKEN") or None


def _resolve_from_python_gitlab_cfg(instance: str) -> str | None:
    """Try ~/.python-gitlab.cfg (INI, configparser)."""
    if not PYTHON_GITLAB_CFG.exists():
        return None
    cfg = configparser.ConfigParser()
    cfg.read(PYTHON_GITLAB_CFG)
    target_url = f"https://{instance}"

    for section in cfg.sections():
        if section == "global":
            continue
        if cfg.get(section, "url", fallback=None) == target_url:
            token = cfg.get(section, "private_token", fallback=None)
            if token:
                return token

    default_section = cfg.get("global", "default", fallback=None)
    if default_section and cfg.has_section(default_section):
        if cfg.get(default_section, "url", fallback=None) == target_url:
            return cfg.get(default_section, "private_token", fallback=None)

    return None


def resolve_token(instance: str, forge_type: str | None = None) -> str | None:
    """Return a token for *instance*, or None if nothing is found.

    Resolution order:
    1. OS keyring (if ``keyring`` package installed)
    2. AUTH_FILE (~/.config/project-hub/auth.json)
    3. forge_type-specific env fallback:
       - "gitlab": GITLAB_TOKEN (gitlab.com only), then ~/.python-gitlab.cfg
       - "github": GITHUB_TOKEN
       - "bitbucket": BITBUCKET_TOKEN
       - None: no env fallbacks
    """
    token = _resolve_from_keyring(instance)
    if token:
        return token

    token = _resolve_from_auth_file(instance)
    if token:
        return token

    if forge_type == "gitlab":
        token = _resolve_from_gitlab_env(instance)
        if token:
            return token
        return _resolve_from_python_gitlab_cfg(instance)

    if forge_type == "github":
        return _resolve_from_github_env(instance)

    if forge_type == "bitbucket":
        return os.environ.get("BITBUCKET_TOKEN")

    return None


def store_token(instance: str, token: str) -> None:
    """Persist *token* for *instance*, preferring OS keyring when available."""
    AUTH_FILE.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
    os.chmod(str(AUTH_FILE.parent), 0o700)

    data: dict[str, str] = {}
    if AUTH_FILE.exists():
        try:
            data = json.loads(AUTH_FILE.read_text())
        except (json.JSONDecodeError, OSError):
            pass

    if _store_to_keyring(instance, token):
        data[instance] = _KEYRING_SENTINEL
        log.info("token for %s stored in OS keyring", instance)
    else:
        data[instance] = token
        log.info("token for %s stored in auth.json (install 'keyring' for encrypted storage)", instance)

    fd = os.open(str(AUTH_FILE), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(fd, 'w') as f:
        json.dump(data, f, indent=2)


def auth_status() -> list[AuthInfo]:
    """Return an AuthInfo entry for every instance recorded in AUTH_FILE."""
    if not AUTH_FILE.exists():
        return []

    try:
        data = json.loads(AUTH_FILE.read_text())
    except (json.JSONDecodeError, OSError):
        return []

    results = []
    for inst, val in data.items():
        if val == _KEYRING_SENTINEL:
            has = _resolve_from_keyring(inst) is not None
            backend = "keyring"
        else:
            has = bool(val)
            backend = "file"
        results.append(AuthInfo(instance=inst, has_token=has, authenticated=has, backend=backend))
    return results


def migration_warning() -> str | None:
    """Return a warning string if file-backed tokens exist and keyring is available."""
    if not _keyring_available():
        return None
    if not AUTH_FILE.exists():
        return None
    try:
        data = json.loads(AUTH_FILE.read_text())
    except (json.JSONDecodeError, OSError):
        return None
    file_backed = [inst for inst, val in data.items() if val != _KEYRING_SENTINEL and val]
    if not file_backed:
        return None
    return (
        f"Tokens for {', '.join(file_backed)} are stored in plaintext. "
        "Run `project-hub auth migrate` to move them to the OS keyring."
    )


def migrate_to_keyring() -> tuple[list[str], list[str]]:
    """Move all file-backed tokens to OS keyring.

    Returns (migrated, failed) instance lists.
    """
    if not AUTH_FILE.exists():
        return [], []
    try:
        data = json.loads(AUTH_FILE.read_text())
    except (json.JSONDecodeError, OSError):
        return [], []

    migrated: list[str] = []
    failed: list[str] = []
    for inst, val in data.items():
        if val == _KEYRING_SENTINEL or not val:
            continue
        if _store_to_keyring(inst, val):
            data[inst] = _KEYRING_SENTINEL
            migrated.append(inst)
        else:
            failed.append(inst)

    if migrated:
        fd = os.open(str(AUTH_FILE), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
        with os.fdopen(fd, 'w') as f:
            json.dump(data, f, indent=2)

    return migrated, failed
