# 경로/파일: realcom-nas-ui/modules/user.py
# 붙여넣기: 이 파일 전체 교체
#
# Ctrl+F 키워드:
# - USERS_JSON
# - SAMBA_CONF_PATH
# - REQUIRED_USERS
# - def add_user(
# - def change_password(
# - def remove_user(
# - def _update_storage_valid_users_conf(

# © Realcom. All rights reserved.
# This software is protected by copyright law.
# Unauthorized copying, modification, or redistribution is prohibited.
#
# 본 소프트웨어는 저작권법의 보호를 받습니다.
# 저작권자의 사전 서면 동의 없이
# 무단 사용, 복제, 수정, 재배포를 금지합니다.
#
# Use of this software constitutes acceptance of the above terms.
# 본 소프트웨어를 사용하는 행위는 위 조건에 동의한 것으로 간주됩니다.

from __future__ import annotations

import os
import json
import re
import subprocess
from typing import Dict, List, Optional, Tuple

from passlib.hash import sha512_crypt

# =========================================================
# PATH / POLICY
# =========================================================

# ✅ 경로만 변경
BASE_DIR = "/opt/realcom-nas"
USERS_JSON = os.path.join(BASE_DIR, "nas_users.json")

# ✅ Samba 설정 파일 (실제 운영 conf)
SAMBA_CONF_PATH = "/etc/samba/nas_smb.conf"

# ✅ valid users에 항상 포함할 계정들 (기본 admin + realnas)
#    필요시 환경변수로 override 가능: "admin realnas"
REQUIRED_USERS = os.environ.get("REALNAS_REQUIRED_SAMBA_USERS", "admin realnas").split()

# 메모리 캐시 (UI 로그인 검증용)
user_passwords: Dict[str, str] = {}

# 기본 admin 계정 해시 (비밀번호: 1)
DEFAULT_ADMIN_HASH = "$6$rounds=656000$8Pn71BWR3cZN54ua$rCeMUKGJ651SHlPPdbw7VYEFEM0wTBAdXDAmu7Np8xRhhKiJRCuW6E4a79dJJL3VTrhRwYLzlFEp2hZ6hphW80"

# 보호 계정 (삭제 금지)
PROTECTED_USERS = {"admin", "realnas"}


# =========================================================
# subprocess helpers (절대 예외 throw 안 함 / sudo -n)
# =========================================================

def _run(cmd: List[str], input_text: Optional[str] = None) -> subprocess.CompletedProcess:
    """
    subprocess wrapper.
    - 실패해도 예외를 밖으로 던지지 않음
    - stdout/stderr 캡처
    """
    try:
        return subprocess.run(
            cmd,
            input=input_text,
            text=True,
            capture_output=True,
            check=False,
        )
    except Exception as e:
        return subprocess.CompletedProcess(cmd, returncode=1, stdout="", stderr=str(e))


def _sudo(cmd: List[str], input_text: Optional[str] = None) -> subprocess.CompletedProcess:
    """
    sudo -n(비번 프롬프트 금지)로 실행.
    - sudoers 미설정이면 returncode!=0로 바로 실패(웹앱 멈춤 없음)
    """
    return _run(["sudo", "-n"] + cmd, input_text=input_text)


def _out(p: subprocess.CompletedProcess) -> str:
    s = (p.stdout or "").strip()
    e = (p.stderr or "").strip()
    if s and e:
        return s + "\n" + e
    return s or e


# =========================================================
# Samba helpers
# =========================================================

def _testparm_ok() -> Tuple[bool, str]:
    """
    반드시 SAMBA_CONF_PATH 대상으로 검사한다.
    """
    p = _run(["testparm", "-s", SAMBA_CONF_PATH])
    return (p.returncode == 0), _out(p)


def _restart_samba() -> Tuple[bool, str]:
    """
    Samba 재시작
    - smbd는 필수로 성공해야 함
    - nmbd는 환경에 따라 없거나(mask) 비활성일 수 있으니 실패해도 전체 실패로 치지 않음
    """
    p1 = _sudo(["systemctl", "restart", "smbd"])
    ok_smbd = (p1.returncode == 0)

    p2 = _sudo(["systemctl", "restart", "nmbd"])
    ok_nmbd = (p2.returncode == 0)

    msg = []
    msg.append(f"[smbd] {'OK' if ok_smbd else 'FAIL'}")
    if _out(p1):
        msg.append(_out(p1))
    msg.append(f"[nmbd] {'OK' if ok_nmbd else 'SKIP/FAIL'}")
    if _out(p2):
        msg.append(_out(p2))

    return ok_smbd, "\n".join(msg).strip()


def _parse_storage_block(conf_text: str):
    """
    [STORAGE] 섹션 블록을 찾아서 (start, end, body_text) 반환
    """
    m = re.search(r"(?ms)^\[STORAGE\]\s*$([\s\S]*?)(?=^\[|\Z)", conf_text)
    if not m:
        return None
    return m.start(), m.end(), m.group(1)


def _update_storage_valid_users_conf(valid_users: List[str]) -> Tuple[bool, str]:
    """
    /etc/samba/nas_smb.conf의 [STORAGE] 섹션 'valid users ='를 자동 갱신한다.
    - UI 사용자 목록 기반으로 valid users 갱신
    - REQUIRED_USERS(기본 admin+realnas) 항상 포함
    - 파일 쓰기는 sudo tee로 한다 (일반 open/write 금지)
    """
    try:
        users = [u.strip() for u in (valid_users or []) if u and u.strip()]
        for ru in REQUIRED_USERS:
            ru = (ru or "").strip()
            if ru and ru not in users:
                users.append(ru)
        users = sorted(set(users))

        if not os.path.exists(SAMBA_CONF_PATH):
            return False, f"conf not found: {SAMBA_CONF_PATH}"

        # 읽기(보통 가능)
        try:
            with open(SAMBA_CONF_PATH, "r", encoding="utf-8") as f:
                content = f.read()
        except Exception as e:
            return False, f"read failed: {e}"

        parsed = _parse_storage_block(content)
        if not parsed:
            return False, "[STORAGE] section not found"

        start, end, body = parsed
        new_line = f"    valid users = {' '.join(users)}"

        if re.search(r"(?m)^\s*valid users\s*=", body):
            body2 = re.sub(r"(?m)^\s*valid users\s*=.*$", new_line, body)
        else:
            body2 = body.rstrip() + "\n" + new_line + "\n"

        new_block = "[STORAGE]\n" + body2.lstrip("\n")
        new_content = content[:start] + new_block + content[end:]

        if new_content != content:
            # 백업(실패해도 진행)
            _sudo(["cp", "-a", SAMBA_CONF_PATH, SAMBA_CONF_PATH + ".bak"])
            # sudo tee로 덮어쓰기
            p = _sudo(["tee", SAMBA_CONF_PATH], input_text=new_content)
            if p.returncode != 0:
                return False, (
                    "write failed (sudo permission needed)\n"
                    f"- target: {SAMBA_CONF_PATH}\n"
                    f"- stderr: {(_out(p) or 'unknown')}"
                )

        ok_tp, out_tp = _testparm_ok()
        if not ok_tp:
            return False, f"testparm failed:\n{out_tp}"

        ok_rs, out_rs = _restart_samba()
        if not ok_rs:
            return False, f"samba restart failed:\n{out_rs}"

        return True, f"ok\n{out_rs}".strip()

    except Exception as e:
        return False, f"exception: {e}"


# =========================================================
# Linux / Samba user operations
# =========================================================

def _ensure_linux_user(username: str) -> Tuple[bool, str]:
    """
    ✅ adduser 사용 금지 (sudoers/환경차로 실패 많음)
    - useradd -m 로 통일
    - 이미 있으면 rc!=0일 수 있으나, 존재 확인 후 무시
    """
    u = (username or "").strip()
    if not u:
        return False, "empty username"

    # 이미 존재하면 OK 처리
    p_exist = _run(["id", u])
    if p_exist.returncode == 0:
        return True, "exists"

    # 생성
    p = _sudo(["/usr/sbin/useradd", "-m", u])
    if p.returncode != 0:
        return False, _out(p) or "useradd failed"
    return True, "created"


def _remove_linux_user(username: str) -> Tuple[bool, str]:
    """
    ✅ deluser 사용 금지 (sudoers/환경차로 실패 많음)
    - userdel -r 로 통일
    - 없으면 OK 처리
    """
    u = (username or "").strip()
    if not u:
        return False, "empty username"

    p_exist = _run(["id", u])
    if p_exist.returncode != 0:
        return True, "not exists"

    p = _sudo(["/usr/sbin/userdel", "-r", u])
    if p.returncode != 0:
        return False, _out(p) or "userdel failed"
    return True, "deleted"


def add_samba_user(username: str, password: str) -> Tuple[bool, str]:
    """
    Samba 사용자 추가/비번설정/활성화
    - 이미 존재해도 비번/활성화는 맞춰줌
    """
    u = (username or "").strip()
    pw = (password or "")
    if not u or not pw:
        return False, "empty username/password"

    # 1) 추가(이미 있으면 rc!=0 가능) — stderr는 참고용
    p1 = _sudo(["/usr/bin/smbpasswd", "-a", u], input_text=f"{pw}\n{pw}\n")
    # 2) 비번 변경(확실히 맞추기)
    p2 = _sudo(["/usr/bin/smbpasswd", u], input_text=f"{pw}\n{pw}\n")
    if p2.returncode != 0:
        return False, f"smbpasswd set password failed:\n{_out(p2) or _out(p1)}"

    # 3) 활성화
    p3 = _sudo(["/usr/bin/smbpasswd", "-e", u])
    if p3.returncode != 0:
        return False, f"smbpasswd -e failed:\n{_out(p3)}"

    # 참고 메시지 구성
    warn = ""
    if p1.returncode != 0 and _out(p1):
        warn = f"(note) smbpasswd -a rc={p1.returncode}:\n{_out(p1)}\n"

    return True, (warn + "ok").strip()


def _remove_samba_user(username: str) -> Tuple[bool, str]:
    u = (username or "").strip()
    if not u:
        return False, "empty username"

    p = _sudo(["/usr/bin/smbpasswd", "-x", u])
    # 이미 없으면 실패할 수도 있으니 강하게 실패 처리 안 함
    if p.returncode != 0:
        return False, _out(p) or "smbpasswd -x failed"
    return True, "deleted"


# =========================================================
# User DB
# =========================================================

def save_users() -> bool:
    try:
        os.makedirs(BASE_DIR, exist_ok=True)
        with open(USERS_JSON, "w", encoding="utf-8") as f:
            json.dump(user_passwords, f, indent=2, ensure_ascii=False)
        return True
    except Exception as e:
        print(f"[ERROR] 사용자 데이터 저장 실패: {e}")
        return False


def load_users() -> None:
    """
    ⚠️ 중요: Flask import 시점에 호출되어도 서버가 죽지 않게 설계
    - 파일 읽기/기본계정 준비만 하고,
    - Samba 반영은 실패해도 예외 던지지 않음
    """
    global user_passwords

    # 1) JSON 로드
    if os.path.exists(USERS_JSON):
        try:
            with open(USERS_JSON, "r", encoding="utf-8") as f:
                user_passwords = json.load(f) or {}
        except Exception as e:
            print(f"[ERROR] 사용자 데이터 로드 실패: {e}")
            user_passwords = {}
    else:
        user_passwords = {}

    # 2) 기본 admin 계정 보장
    if "admin" not in user_passwords:
        user_passwords["admin"] = DEFAULT_ADMIN_HASH
        save_users()

        # admin 리눅스/삼바 등록은 "시도만" 하고 실패는 무시(앱 기동 안정성)
        try:
            _ensure_linux_user("admin")
            add_samba_user("admin", "1")
        except Exception as e:
            print(f"[WARN] 기본 admin samba/linux 세팅 실패(무시): {e}")

    # 3) import 시점 samba conf sync는 "시도만" 하고 실패는 무시
    try:
        ok, msg = _update_storage_valid_users_conf(list_users())
        if not ok:
            print(f"[WARN] 초기 samba valid users sync 실패(무시): {msg}")
    except Exception as e:
        print(f"[WARN] 초기 samba valid users sync 예외(무시): {e}")


def verify_password(username: str, password: str) -> bool:
    hashed = user_passwords.get(username)
    if not hashed:
        return False
    try:
        return sha512_crypt.verify(password, hashed)
    except Exception:
        return False


# =========================================================
# Validation
# =========================================================

def _validate_username(username: str) -> Tuple[bool, str]:
    """
    - UI maxlength=16 기준
    - 리눅스/Samba 계정으로 안전한 패턴만 허용
    """
    u = (username or "").strip()
    if not u:
        return False, "empty username"
    if len(u) > 16:
        return False, "username too long (max 16)"
    if not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9._-]{0,15}", u):
        return False, "invalid username format"
    return True, u


# =========================================================
# 사용자 관리 (UI에서 호출)
# - 기존 라우트가 bool만 기대하니, 여기서는 bool 반환 유지
# - 대신 서버 로그에 실패 원인을 최대한 남김
# =========================================================

def add_user(username: str, password: str) -> bool:
    global user_passwords

    ok_u, u_or_msg = _validate_username(username)
    if not ok_u:
        print(f"[WARN] add_user invalid username: {u_or_msg}")
        return False
    u = u_or_msg
    pw = (password or "").strip()
    if not pw:
        print("[WARN] add_user empty password")
        return False

    if u in user_passwords:
        print(f"[WARN] 사용자 '{u}' 이미 존재")
        return False

    try:
        # 1) JSON DB 저장
        hashed = sha512_crypt.hash(pw)
        user_passwords[u] = hashed
        if not save_users():
            user_passwords.pop(u, None)
            return False

        # 2) 리눅스 유저 생성 (useradd -m)
        ok_lx, msg_lx = _ensure_linux_user(u)
        if not ok_lx:
            print(f"[ERROR] linux user create failed: {msg_lx}")
            # 롤백
            user_passwords.pop(u, None)
            save_users()
            return False

        # 3) Samba 유저 추가/비번/활성화
        ok_sm, msg_sm = add_samba_user(u, pw)
        if not ok_sm:
            print(f"[ERROR] samba user create/set failed: {msg_sm}")
            # 롤백(앱 DB만)
            user_passwords.pop(u, None)
            save_users()
            return False

        # 4) Samba 공유 valid users 자동 반영 + 재시작
        ok_vu, msg_vu = _update_storage_valid_users_conf(list_users())
        if not ok_vu:
            print(f"[WARN] valid users sync failed (ignored): {msg_vu}")
            # 사용자 자체는 추가됐으니 False로 만들지 말고 True 유지(운영 편의)
            # 단, 공유 접근이 안 될 수 있으니 로그 남김

        print(f"[INFO] 사용자 '{u}' 추가 완료 (linux={msg_lx}, samba={msg_sm})")
        return True

    except Exception as e:
        print(f"[ERROR] 사용자 추가 실패: {e}")
        return False


def change_password(username: str, new_password: str) -> bool:
    ok_u, u_or_msg = _validate_username(username)
    if not ok_u:
        print(f"[WARN] change_password invalid username: {u_or_msg}")
        return False
    u = u_or_msg

    pw = (new_password or "").strip()
    if not pw:
        return False

    if u not in user_passwords:
        print(f"[WARN] 사용자 '{u}' 존재하지 않음")
        return False

    try:
        # 1) JSON DB 갱신
        user_passwords[u] = sha512_crypt.hash(pw)
        if not save_users():
            return False

        # 2) Samba 비번 변경
        p = _sudo(["/usr/bin/smbpasswd", u], input_text=f"{pw}\n{pw}\n")
        if p.returncode != 0:
            print(f"[ERROR] Samba 비밀번호 변경 실패: {_out(p)}")
            return False

        print(f"[INFO] 사용자 '{u}' 비밀번호 변경 완료")
        return True

    except Exception as e:
        print(f"[ERROR] 비밀번호 변경 예외: {e}")
        return False


def remove_user(username: str) -> bool:
    global user_passwords

    ok_u, u_or_msg = _validate_username(username)
    if not ok_u:
        print(f"[WARN] remove_user invalid username: {u_or_msg}")
        return False
    u = u_or_msg

    if u in PROTECTED_USERS:
        print(f"[WARN] '{u}' 계정은 삭제할 수 없음")
        return False

    if u not in user_passwords:
        print(f"[WARN] 사용자 '{u}' 존재하지 않음")
        return False

    try:
        # 1) JSON DB에서 제거
        old_hash = user_passwords.get(u)
        user_passwords.pop(u, None)
        if not save_users():
            if old_hash:
                user_passwords[u] = old_hash
            return False

        # 2) Samba 사용자 삭제(실패하면 로그만)
        ok_sm, msg_sm = _remove_samba_user(u)
        if not ok_sm:
            print(f"[WARN] samba user delete failed (ignored): {msg_sm}")

        # 3) 시스템 사용자 삭제(userdel -r)
        ok_lx, msg_lx = _remove_linux_user(u)
        if not ok_lx:
            print(f"[WARN] linux user delete failed (ignored): {msg_lx}")

        # 4) Samba 공유 valid users 자동 반영 + 재시작
        ok_vu, msg_vu = _update_storage_valid_users_conf(list_users())
        if not ok_vu:
            print(f"[WARN] valid users sync failed (ignored): {msg_vu}")

        print(f"[INFO] 사용자 '{u}' 삭제 완료 (samba={msg_sm}, linux={msg_lx})")
        return True

    except Exception as e:
        print(f"[ERROR] 사용자 삭제 실패: {e}")
        return False


def list_users() -> list:
    # UI에서 바로 쓰는 형태 유지
    return list(user_passwords.keys())


# =========================================================
# module import init
# =========================================================
load_users()
