# 경로/파일: modules/share.py
# Ctrl+F: "### Samba Sync ###" / "def sync_samba_conf"
# 설명:
# - nas_shares.json 기반 공유폴더 관리
# - nginx htpasswd / conf 생성
# - samba conf(/etc/samba/nas_smb.conf) 생성 + testparm 체크 + smbd 재시작(가능하면 nmbd)
# - Flask에서 호출해도 sudo 비번 프롬프트로 멈추지 않게 sudo -n 사용

import os
import json
import shutil
import pwd
import grp
import subprocess
from typing import List, Dict, Tuple, Optional

from passlib.apache import HtpasswdFile

BASE_DIR = "/opt/realcom-nas"

# ----- 기본 경로 상수 -----
SHARES_JSON = os.path.join(BASE_DIR, "nas_shares.json")
USERS_JSON = os.path.join(BASE_DIR, "nas_users.json")
HTPASSWD_DIR = os.path.join(BASE_DIR, "htpasswd")
NGINX_CONF_PATH = os.path.join(BASE_DIR, "nas_shares.conf")
DEFAULT_SHARE_PATH_FILE = os.path.join(BASE_DIR, "nas_default_share")

# Samba
SAMBA_CONF_PATH = "/etc/samba/nas_smb.conf"

# 권한/소유자 설정
SHARE_OWNER = "realnas"
SHARE_GROUP = "realnas"
PERM_DIR = 0o755
PERM_FILE = 0o644

# share group(공유폴더 협업용)
SHARE_DATA_GROUP = "share_data"


# -------------------------
# 공통 유틸
# -------------------------
def ensure_dir(path: str):
    os.makedirs(path, exist_ok=True)


def _run(cmd: List[str], input_text: Optional[str] = None) -> subprocess.CompletedProcess:
    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(비번 프롬프트 금지). 비번 필요하면 바로 실패(returncode != 0)
    """
    return _run(["sudo", "-n"] + cmd, input_text=input_text)


def set_owner_and_permission(path: str, perm: int = PERM_DIR):
    """생성된 파일/폴더 권한 및 소유자 자동 변경(권한 없으면 경고만)"""
    try:
        uid = pwd.getpwnam(SHARE_OWNER).pw_uid
        gid = grp.getgrnam(SHARE_GROUP).gr_gid
        os.chown(path, uid, gid)
        os.chmod(path, perm)
    except Exception as e:
        print(f"[WARN] 권한/소유자 설정 실패: {e}")


# -------------------------
# 사용자 JSON
# -------------------------
def load_users() -> List[dict]:
    ensure_dir(os.path.dirname(USERS_JSON))
    if not os.path.exists(USERS_JSON):
        with open(USERS_JSON, "w", encoding="utf-8") as f:
            json.dump([], f)
    with open(USERS_JSON, "r", encoding="utf-8") as f:
        txt = f.read().strip()
        if not txt:
            return []
        return json.loads(txt)


def list_users() -> List[dict]:
    return load_users()


# -------------------------
# shares JSON
# -------------------------
def load_shares() -> List[dict]:
    try:
        if not os.path.exists(SHARES_JSON):
            return []
        with open(SHARES_JSON, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception as e:
        print(f"[ERROR] 공유폴더 목록 로드 실패: {e}")
        return []


def save_shares(shares: List[dict]) -> bool:
    try:
        ensure_dir(os.path.dirname(SHARES_JSON))
        with open(SHARES_JSON, "w", encoding="utf-8") as f:
            json.dump(shares, f, indent=2, ensure_ascii=False)
        return True
    except Exception as e:
        print(f"[ERROR] 공유폴더 목록 저장 실패: {e}")
        return False


def list_share() -> List[dict]:
    return load_shares()


def get_share(name: str) -> Optional[dict]:
    if not name:
        return None
    name = name.strip()
    for s in load_shares():
        if (s.get("name") or "").strip() == name:
            return s
    return None


# -------------------------
# htpasswd
# -------------------------
def get_htpasswd_path(share_name: str) -> str:
    ensure_dir(HTPASSWD_DIR)
    return os.path.join(HTPASSWD_DIR, f".htpasswd_{share_name.strip()}")


def make_htpasswd_hash(password: str, username: str = "user") -> str:
    ht = HtpasswdFile()
    ht.set_password(username, password)
    return ht.to_string().split(":", 1)[1].strip()


def update_htpasswd_for_share(share_name: str, allowed_users: List[str]):
    """
    nas_users.json 안에 password_hash(또는 password) 있는 사용자 중
    allowed_users에 포함된 계정만 해당 공유폴더 htpasswd로 생성
    """
    htpasswd_file = get_htpasswd_path(share_name)
    all_users = load_users()

    allowed_set = set([(u or "").strip() for u in allowed_users if (u or "").strip()])
    with open(htpasswd_file, "w", encoding="utf-8") as f:
        for u in all_users:
            if not isinstance(u, dict):
                continue
            uid = (u.get("id") or "").strip()
            if not uid:
                continue
            if uid not in allowed_set:
                continue

            if "password_hash" in u and u["password_hash"]:
                f.write(f"{uid}:{u['password_hash']}\n")
            elif "password" in u and u["password"]:
                hash_pw = make_htpasswd_hash(u["password"])
                f.write(f"{uid}:{hash_pw}\n")

    set_owner_and_permission(htpasswd_file, perm=0o640)


# -------------------------
# 대표 공유폴더
# -------------------------
def set_default_share(share_name: str) -> bool:
    try:
        ensure_dir(os.path.dirname(DEFAULT_SHARE_PATH_FILE))
        with open(DEFAULT_SHARE_PATH_FILE, "w", encoding="utf-8") as f:
            f.write(share_name.strip() if share_name else "")
        print(f"[DEBUG] set_default_share() 저장 완료: '{share_name}'")
        return True
    except Exception as e:
        print(f"[ERROR] 대표 공유폴더 저장 실패: {e}")
        return False


def get_default_share() -> Optional[str]:
    try:
        with open(DEFAULT_SHARE_PATH_FILE, "r", encoding="utf-8") as f:
            val = f.read().strip()
            print(f"[DEBUG] get_default_share() 반환값: '{val}'")
            return val
    except Exception as e:
        print(f"[ERROR] get_default_share() 읽기 실패: {e}")
        return None


def get_default_share_path() -> Optional[str]:
    default_share = get_default_share()
    if not default_share:
        return None
    for s in list_share():
        if s.get("name") == default_share:
            return s.get("path")
    return None


# -------------------------
# 권한/그룹 자동화(선택)
# -------------------------
def ensure_share_group_and_users(users: List[str], group: str = SHARE_DATA_GROUP):
    """
    - group 없으면 생성
    - allowed_users + realnas을 group에 추가
    """
    # group create
    try:
        grp.getgrnam(group)
    except KeyError:
        _sudo(["groupadd", group])

    for u in set([(x or "").strip() for x in (users or [])] + ["realnas"]):
        if not u:
            continue
        _sudo(["usermod", "-aG", group, u])


def auto_fix_permissions(path: str, owner: str = "realnas", group: str = SHARE_DATA_GROUP):
    """
    전체 재귀 소유자:그룹 및 권한 (디렉토리 2770, 파일 0660)
    """
    _sudo(["chown", "-R", f"{owner}:{group}", path])
    _sudo(["find", path, "-type", "d", "-exec", "chmod", "2770", "{}", "+"])
    _sudo(["find", path, "-type", "f", "-exec", "chmod", "0660", "{}", "+"])


# -------------------------
# 공유폴더 add/update/remove
# -------------------------
def add_share(name: str, path: str, allowed_users: List[str]) -> Tuple[bool, str]:
    name = (name or "").strip()
    path = os.path.abspath((path or "").strip())
    shares = list_share()

    if not name or not path:
        return False, "이름/경로가 비어있음"

    DISALLOWED_PATHS = [
        "/", "/root", "/home", "/opt/realcom-nas",
        "/boot", "/etc", "/bin", "/usr", "/tmp"
    ]
    for dis in DISALLOWED_PATHS:
        if path == dis or path.startswith(dis + "/"):
            return False, f"❌ 잘못된 경로입니다: {dis} 이하에는 공유폴더를 만들 수 없습니다."

    if any((s.get("name") or "").strip() == name for s in shares):
        return False, "이미 존재하는 공유폴더 이름입니다."

    if not os.path.exists(path):
        print(f"[WARN] 지정된 경로가 존재하지 않음: {path} (마운트 안 됐을 수도 있음)")

    try:
        os.makedirs(path, exist_ok=True)
        set_owner_and_permission(path, perm=PERM_DIR)

        # 그룹/권한 자동화(원하면 유지)
        ensure_share_group_and_users(allowed_users, group=SHARE_DATA_GROUP)
        auto_fix_permissions(path, owner="realnas", group=SHARE_DATA_GROUP)

    except Exception as e:
        return False, f"폴더 생성/권한설정 오류: {e}"

    share = {
        "name": name,
        "path": path,
        "allowed_users": [(u or "").strip() for u in (allowed_users or []) if (u or "").strip()],
    }
    shares.append(share)
    save_shares(shares)

    update_htpasswd_for_share(name, share["allowed_users"])
    sync_nginx_conf()
    ok, msg = sync_samba_conf()   # ✅ samba도 같이 반영
    if not ok:
        return True, f"공유폴더 '{name}' 추가됨. (단, Samba 반영 실패)\n{msg}"

    return True, f"공유폴더 '{name}'가 추가되었습니다. (경로 유효성: {'있음' if os.path.exists(path) else '없음'})"


def update_share(name: str, allowed_users: List[str]) -> Tuple[bool, str]:
    name = (name or "").strip()
    allowed_users = [(u or "").strip() for u in (allowed_users or []) if (u or "").strip()]

    shares = load_shares()
    found = False
    for s in shares:
        if (s.get("name") or "").strip() == name:
            s["allowed_users"] = allowed_users
            found = True
            break

    if not found:
        return False, "공유폴더를 찾을 수 없습니다."

    save_shares(shares)
    update_htpasswd_for_share(name, allowed_users)
    sync_nginx_conf()

    ok, msg = sync_samba_conf()
    if not ok:
        return True, f"공유폴더 '{name}' 권한 수정됨. (단, Samba 반영 실패)\n{msg}"

    return True, f"공유폴더 '{name}'의 접근 권한이 수정되었습니다."


def remove_share(name: str) -> Tuple[bool, str]:
    name = (name or "").strip()
    shares = [s for s in load_shares() if (s.get("name") or "").strip() != name]
    save_shares(shares)

    htpasswd_path = get_htpasswd_path(name)
    if os.path.exists(htpasswd_path):
        os.remove(htpasswd_path)

    default_share = get_default_share()
    if default_share and default_share.strip() == name:
        set_default_share("")

    sync_nginx_conf()
    ok, msg = sync_samba_conf()
    if not ok:
        return True, f"공유폴더 '{name}' 삭제됨. (단, Samba 반영 실패)\n{msg}"

    return True, f"공유폴더 '{name}'가 삭제되었습니다."


# -------------------------
# Nginx conf 자동 동기화
# -------------------------
def sync_nginx_conf() -> bool:
    shares = load_shares()
    conf_lines = [
        "# 자동 생성 - 공유폴더별 접근제어\n",
    ]
    for s in shares:
        share_name = (s.get("name") or "").strip()
        share_path = (s.get("path") or "").strip()
        if not share_name or not share_path:
            continue

        htpasswd_path = get_htpasswd_path(share_name)
        conf_lines.append(f"""
location /share/{share_name}/ {{
    auth_basic "Restricted Access";
    auth_basic_user_file {htpasswd_path};
    limit_except GET POST OPTIONS {{
        deny all;
    }}
    root {share_path};
    client_max_body_size 0;
    autoindex off;
}}
""")

    try:
        ensure_dir(os.path.dirname(NGINX_CONF_PATH))
        with open(NGINX_CONF_PATH, "w", encoding="utf-8") as f:
            f.writelines(conf_lines)

        r = _sudo(["/usr/sbin/nginx", "-t"])
        if r.returncode != 0:
            return False

        _sudo(["/usr/sbin/nginx", "-s", "reload"])
        print("[INFO] Nginx 설정 동기화 및 리로드 완료")
        return True

    except Exception as e:
        print(f"[ERROR] Nginx 설정 동기화 실패: {e}")
        return False


# -------------------------
# ### Samba Sync ###
# -------------------------
def _write_samba_conf_text(shares: List[dict]) -> str:
    """
    nas_shares.json → samba conf 텍스트 생성
    - guest ok는 기본 NO(안전)
    - valid users는 공백 나열 (realnas 강제 포함)
    """
    lines = ["# 자동 생성 - 공유폴더별 Samba 설정\n\n"]

    for s in shares:
        share_name = (s.get("name") or "").strip()
        share_path = (s.get("path") or "").strip()
        if not share_name or not share_path:
            continue

        allowed_users = [(u or "").strip() for u in (s.get("allowed_users") or []) if (u or "").strip()]
        if "realnas" not in allowed_users:
            allowed_users.append("realnas")
        allowed_users = sorted(set(allowed_users))

        valid_users_str = " ".join(allowed_users)

        # 🔥 여기 옵션은 너 정책에 맞게 조절 가능
        lines.append(f"""
[{share_name}]
    path = {share_path}
    browseable = yes
    writable = yes
    read only = no

    valid users = {valid_users_str}
    guest ok = no

    create mask = 0660
    directory mask = 2770

    # 락/오플락은 환경 따라 문제 유발 가능 → 기본은 무난하게
    locking = yes
    strict locking = yes
    oplocks = yes
    level2 oplocks = yes
""")

    return "\n".join(lines).strip() + "\n"


def sync_samba_conf() -> Tuple[bool, str]:
    """
    /etc/samba/nas_smb.conf 생성 → testparm 검사 → smbd 재시작(가능하면 nmbd)
    - sudo -n 사용: 비번 필요하면 바로 실패(Flask 멈춤 방지)
    """
    shares = load_shares()
    conf_text = _write_samba_conf_text(shares)

    # 1) 백업
    _sudo(["cp", "-a", SAMBA_CONF_PATH, SAMBA_CONF_PATH + ".bak"])

    # 2) 쓰기 (권한 때문에 tee 사용)
    w = _sudo(["tee", SAMBA_CONF_PATH], input_text=conf_text)
    if w.returncode != 0:
        msg = (w.stderr or w.stdout or "").strip()
        return False, f"write samba conf failed: {msg}"

    # 3) 문법 검사 (대상 파일 지정)
    t = _run(["testparm", "-s", SAMBA_CONF_PATH])
    if t.returncode != 0:
        return False, f"testparm failed:\n{(t.stdout or '')}\n{(t.stderr or '')}".strip()

    # 4) 재시작 (smbd 필수, nmbd는 optional)
    r1 = _sudo(["systemctl", "restart", "smbd"])
    if r1.returncode != 0:
        return False, f"smbd restart failed:\n{(r1.stdout or '')}\n{(r1.stderr or '')}".strip()

    r2 = _sudo(["systemctl", "restart", "nmbd"])
    # nmbd 실패는 참고만(환경 따라 없음)
    msg2 = ""
    if r2.returncode != 0:
        msg2 = f"\n[nmbd] restart failed/skip:\n{(r2.stdout or '')}\n{(r2.stderr or '')}".strip()

    return True, "Samba 설정 동기화 및 서비스 재시작 완료" + (("\n" + msg2) if msg2 else "")


# -------------------------
# 공유폴더 후보 목록(대표폴더 하위)
# -------------------------
EXCLUDE_FOLDERS = {"media"}  # 여러 개 추가 가능

def get_candidate_paths(base_dir: str) -> List[str]:
    candidates: List[str] = []
    if not os.path.exists(base_dir):
        return candidates
    try:
        for entry in os.scandir(base_dir):
            if entry.is_dir() and entry.name not in EXCLUDE_FOLDERS:
                candidates.append(entry.path)
    except Exception as e:
        print(f"[ERROR] 폴더 후보 조회 실패: {e}")
    return candidates


# -------------------------
# 파일/폴더 생성 안전 도우미
# -------------------------
def safe_create_folder(path: str):
    os.makedirs(path, exist_ok=True)
    set_owner_and_permission(path, perm=0o777)

def safe_save_file(file_obj, dest_path: str):
    file_obj.save(dest_path)
    set_owner_and_permission(dest_path, perm=0o777)

def safe_copy_file(src: str, dest: str):
    shutil.copy2(src, dest)
    set_owner_and_permission(dest, perm=0o777)

def safe_unzip(zip_path: str, extract_to: str):
    import zipfile
    with zipfile.ZipFile(zip_path, "r") as zf:
        zf.extractall(extract_to)
    for root, dirs, files in os.walk(extract_to):
        for d in dirs:
            set_owner_and_permission(os.path.join(root, d), perm=0o777)
        for f in files:
            set_owner_and_permission(os.path.join(root, f), perm=0o777)
