"""
D-1: LDAP/AD 연동 엔진

기능:
  1. LDAP/Active Directory 인증 (바인드 테스트)
  2. 사용자 정보 조회 및 동기화
  3. 그룹 멤버십 → 역할(Role) 매핑
  4. Fallback: LDAP 미연결 시 로컬 DB 인증
  5. LDAP 연결 설정 암호화 저장 지원

보안 제약:
  - LDAP 서버 자격증명 노출 금지
  - 바인드 DN/패스워드는 서버 환경변수 또는 암호화 DB 저장
  - 외부 LDAP 서버 URL만 지원 (내부망 전용)
"""
from __future__ import annotations

import logging
import os
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple

logger = logging.getLogger(__name__)

# ── LDAP 설정 ─────────────────────────────────────────────────────────────────

@dataclass
class LDAPConfig:
    """LDAP 연결 설정."""
    server_url:     str = ""          # ldap://192.168.0.10:389
    base_dn:        str = ""          # DC=company,DC=local
    bind_dn:        str = ""          # CN=svc-ldap,OU=ServiceAccounts,DC=company,DC=local
    bind_password:  str = ""          # (절대 로그 출력 금지)
    user_search_base: str = ""        # OU=Users,DC=company,DC=local
    user_filter:    str = "(sAMAccountName={username})"  # AD 기본 필터
    group_search_base: str = ""       # OU=Groups,DC=company,DC=local
    group_attr:     str = "memberOf"  # 그룹 멤버십 속성
    uid_attr:       str = "sAMAccountName"    # 사용자명 속성
    email_attr:     str = "mail"
    display_name_attr: str = "displayName"
    department_attr: str = "department"
    use_ssl:        bool = False
    use_tls:        bool = False
    timeout:        int  = 10
    enabled:        bool = False      # LDAP 인증 활성화 여부


# 그룹 → 역할 매핑 기본값
DEFAULT_GROUP_ROLE_MAP: Dict[str, str] = {
    "GUARDiA-ADMIN":    "ADMIN",
    "GUARDiA-PM":       "PM",
    "GUARDiA-ENGINEER": "ENGINEER",
    "GUARDiA-VIEWER":   "VIEWER",
    "Domain Admins":    "ADMIN",
    "IT-Operations":    "ENGINEER",
    "IT-Management":    "PM",
}


def _load_config_from_env() -> LDAPConfig:
    """환경변수에서 LDAP 설정 로드."""
    return LDAPConfig(
        server_url      = os.environ.get("LDAP_SERVER_URL", ""),
        base_dn         = os.environ.get("LDAP_BASE_DN", ""),
        bind_dn         = os.environ.get("LDAP_BIND_DN", ""),
        bind_password   = os.environ.get("LDAP_BIND_PASSWORD", ""),
        user_search_base = os.environ.get("LDAP_USER_SEARCH_BASE", ""),
        group_search_base = os.environ.get("LDAP_GROUP_SEARCH_BASE", ""),
        use_ssl         = os.environ.get("LDAP_USE_SSL", "false").lower() == "true",
        use_tls         = os.environ.get("LDAP_USE_TLS", "false").lower() == "true",
        enabled         = os.environ.get("LDAP_ENABLED", "false").lower() == "true",
    )


# 런타임 설정 (init_ldap_config()로 초기화)
_current_config: Optional[LDAPConfig] = None
_group_role_map: Dict[str, str] = dict(DEFAULT_GROUP_ROLE_MAP)


def init_ldap_config(config: Optional[LDAPConfig] = None) -> LDAPConfig:
    """LDAP 설정 초기화. config가 None이면 환경변수에서 로드."""
    global _current_config
    _current_config = config or _load_config_from_env()
    return _current_config


def get_ldap_config() -> LDAPConfig:
    """현재 LDAP 설정 반환."""
    global _current_config
    if _current_config is None:
        _current_config = _load_config_from_env()
    return _current_config


def set_group_role_map(mapping: Dict[str, str]) -> None:
    """그룹→역할 매핑 업데이트."""
    global _group_role_map
    _group_role_map.update(mapping)


def map_groups_to_role(groups: List[str]) -> str:
    """
    LDAP 그룹 목록에서 ITSM 역할을 매핑.
    가장 높은 권한 역할 반환.
    """
    role_priority = {"ADMIN": 4, "PM": 3, "ENGINEER": 2, "VIEWER": 1}
    best_role = "VIEWER"
    best_prio = 0

    for grp in groups:
        # 완전 일치
        if grp in _group_role_map:
            role = _group_role_map[grp]
            prio = role_priority.get(role, 0)
            if prio > best_prio:
                best_prio = prio
                best_role = role
        else:
            # 그룹 이름 부분 일치 (CN=... 형식 지원)
            for map_grp, role in _group_role_map.items():
                if map_grp.lower() in grp.lower():
                    prio = role_priority.get(role, 0)
                    if prio > best_prio:
                        best_prio = prio
                        best_role = role

    return best_role


# ── LDAP 인증 함수 ─────────────────────────────────────────────────────────────

def _try_import_ldap3():
    """ldap3 패키지 임포트 시도. 없으면 None 반환."""
    try:
        import ldap3  # type: ignore
        return ldap3
    except ImportError:
        return None


def authenticate_ldap(
    username: str,
    password: str,
    config: Optional[LDAPConfig] = None,
) -> Tuple[bool, Optional[Dict], Optional[str]]:
    """
    LDAP 인증 수행.
    Returns: (success, user_info_dict, error_message)
    - user_info_dict: {"username", "email", "display_name", "department", "groups", "role"}
    - error_message: 실패 시 오류 메시지
    """
    cfg = config or get_ldap_config()

    if not cfg.enabled or not cfg.server_url:
        return False, None, "LDAP 비활성화 상태"

    ldap3 = _try_import_ldap3()
    if ldap3 is None:
        return False, None, "ldap3 패키지 미설치 (pip install ldap3)"

    try:
        # 서버 연결
        server = ldap3.Server(
            cfg.server_url,
            use_ssl = cfg.use_ssl,
            connect_timeout = cfg.timeout,
        )

        # 서비스 계정으로 바인드하여 사용자 DN 조회
        with ldap3.Connection(
            server,
            user     = cfg.bind_dn,
            password = cfg.bind_password,
            auto_bind = ldap3.AUTO_BIND_NO_TLS,
        ) as conn:
            if cfg.use_tls:
                conn.start_tls()

            # 사용자 검색
            user_filter = cfg.user_filter.format(username=username)
            attrs = [
                cfg.uid_attr, cfg.email_attr,
                cfg.display_name_attr, cfg.department_attr,
                cfg.group_attr, "cn", "dn",
            ]
            conn.search(
                search_base   = cfg.user_search_base or cfg.base_dn,
                search_filter = user_filter,
                attributes    = attrs,
            )

            if not conn.entries:
                return False, None, f"사용자 '{username}'를 찾을 수 없습니다."

            entry    = conn.entries[0]
            user_dn  = entry.entry_dn

        # 사용자 자격증명으로 바인드 (인증)
        with ldap3.Connection(
            server,
            user     = user_dn,
            password = password,
            auto_bind = ldap3.AUTO_BIND_NO_TLS,
        ) as user_conn:
            if not user_conn.bound:
                return False, None, "잘못된 비밀번호"

        # 그룹 추출
        groups_raw = []
        if hasattr(entry, cfg.group_attr):
            groups_raw = [str(g) for g in getattr(entry, cfg.group_attr)]

        # CN 추출 (CN=GroupName,OU=...)
        groups = []
        for g in groups_raw:
            if g.startswith("CN="):
                cn = g.split(",")[0].replace("CN=", "")
                groups.append(cn)
            else:
                groups.append(g)

        role = map_groups_to_role(groups)

        user_info = {
            "username":     str(getattr(entry, cfg.uid_attr, username)),
            "email":        str(getattr(entry, cfg.email_attr, "")) or None,
            "display_name": str(getattr(entry, cfg.display_name_attr, username)),
            "department":   str(getattr(entry, cfg.department_attr, "")) or None,
            "groups":       groups,
            "role":         role,
            "dn":           user_dn,
        }

        logger.info("LDAP 인증 성공: %s (role=%s)", username, role)
        return True, user_info, None

    except Exception as e:
        logger.warning("LDAP 인증 실패 (%s): %s", username, str(e)[:100])
        return False, None, str(e)[:100]


async def sync_ldap_user(
    db,
    username: str,
    ldap_info: Dict,
) -> Optional[object]:
    """
    LDAP 인증 성공 후 로컬 DB와 동기화.
    - 신규 사용자: 자동 생성
    - 기존 사용자: 이메일/역할 업데이트
    Returns: User ORM 객체
    """
    from sqlalchemy import select as sa_select
    from models import User, UserRole

    try:
        q = sa_select(User).where(User.username == username)
        user = (await db.execute(q)).scalars().first()

        role_map = {
            "ADMIN": UserRole.ADMIN,
            "PM":    UserRole.PM,
            "ENGINEER": UserRole.ENGINEER,
            "VIEWER": UserRole.VIEWER,
        }
        role_enum = role_map.get(ldap_info.get("role", "VIEWER"), UserRole.VIEWER)

        if user is None:
            # 신규 사용자 자동 생성
            from core.auth import get_password_hash
            import secrets
            user = User(
                username     = username,
                email        = ldap_info.get("email") or f"{username}@ldap.local",
                hashed_pw    = get_password_hash(secrets.token_hex(16)),  # 임시 패스워드
                role         = role_enum,
                is_active    = True,
                display_name = ldap_info.get("display_name"),
                department   = ldap_info.get("department"),
                auth_type    = "LDAP",
                must_change_pw = False,   # LDAP 인증 사용자는 로컬 PW 변경 불필요
            )
            db.add(user)
            await db.flush()
            logger.info("LDAP 신규 사용자 생성: %s (%s)", username, role_enum)
        else:
            # 기존 사용자 업데이트
            if ldap_info.get("email"):
                user.email = ldap_info["email"]
            user.role = role_enum
            if ldap_info.get("department"):
                user.department = ldap_info["department"]

        await db.commit()
        await db.refresh(user)
        return user

    except Exception as e:
        logger.error("LDAP 사용자 동기화 오류 (%s): %s", username, e)
        await db.rollback()
        return None


def test_ldap_connection(config: Optional[LDAPConfig] = None) -> Dict:
    """LDAP 연결 테스트 (ping only)."""
    cfg = config or get_ldap_config()

    if not cfg.enabled:
        return {"success": False, "reason": "LDAP 비활성화"}
    if not cfg.server_url:
        return {"success": False, "reason": "server_url 미설정"}

    ldap3 = _try_import_ldap3()
    if ldap3 is None:
        return {"success": False, "reason": "ldap3 미설치"}

    try:
        server = ldap3.Server(
            cfg.server_url,
            use_ssl = cfg.use_ssl,
            connect_timeout = cfg.timeout,
        )
        with ldap3.Connection(server, user=cfg.bind_dn, password=cfg.bind_password) as conn:
            bound = conn.bound
        return {
            "success":    bound,
            "server_url": cfg.server_url,
            "bind_dn":    cfg.bind_dn,
            "reason":     "연결 성공" if bound else "바인드 실패",
        }
    except Exception as e:
        return {"success": False, "reason": str(e)[:100]}
