# 경로/파일: realcom-nas-ui/modules/share.py
# 붙여넣기: 이 파일 전체 교체
# 정책: 공유폴더 path는 /mnt/storage 또는 /mnt/usb* 하위만 허용
# Ctrl+F:
#   - def _is_allowed_share_path
#   - def add_share
#   - create_folder  (폴더 생성 옵션)

# © 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.
# 본 소프트웨어를 사용하는 행위는 위 조건에 동의한 것으로 간주됩니다.

import os
import json
import shutil
import pwd
import grp
import re
import subprocess
from typing import List, 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"

# ✅ 보안 기본값(판매용이면 777 금지)
PERM_DIR = 0o2770
PERM_FILE = 0o0660

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

# ✅ 공유폴더 허용 루트(정책)
ALLOWED_SHARE_ROOTS = ["/mnt/storage"]  # + /mnt/usb* 는 정규식으로 허용


# -------------------------
# 공통 유틸
# -------------------------
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}")


def _realpath(path: str) -> str:
    try:
        return os.path.realpath(path)
    except Exception:
        return os.path.abspath(path)


def _is_under(child: str, parent: str) -> bool:
    c = _realpath(child).rstrip("/")
    p = _realpath(parent).rstrip("/")
    return c == p or c.startswith(p + "/")


def _is_allowed_share_path(path: str) -> Tuple[bool, str]:
    """
    ✅ 공유폴더는 /mnt/storage 또는 /mnt/usb* 하위만 허용
    - /media 자동마운트, /, /opt 등은 전부 차단
    """
    p = (path or "").strip()
    if not p:
        return False, "경로가 비어있음"
    if not p.startswith("/"):
        return False, "경로는 절대경로(/로 시작)만 허용"

    p = os.path.normpath(p)

    # OS/시스템 보호 (절대 금지)
    blocked_exact = {
        "/", "/boot", "/boot/efi", "/etc", "/bin", "/sbin", "/usr", "/var",
        "/proc", "/sys", "/dev", "/run", "/root", "/home", "/opt"
    }
    if p in blocked_exact:
        return False, f"위험 경로는 공유 불가: {p}"
    for bx in ("/boot", "/etc", "/bin", "/sbin", "/usr", "/var", "/proc", "/sys", "/dev", "/run", "/root", "/home", "/opt"):
        if p.startswith(bx + "/"):
            return False, f"위험 경로는 공유 불가: {bx} 하위"

    # /media 는 자동마운트 영역 → 공유/elfinder에 쓰면 사고 + 해제됨
    if p == "/media" or p.startswith("/media/"):
        return False, "/media/* 자동마운트 경로는 공유폴더로 금지"

    # ✅ 허용 루트 체크
    ok = False
    for r in ALLOWED_SHARE_ROOTS:
        if _is_under(p, r):
            ok = True
            break
    if not ok and re.match(r"^/mnt/usb\d+($|/)", p):
        ok = True

    if not ok:
        return False, "허용 경로 아님 (허용: /mnt/storage 또는 /mnt/usb*)"

    return True, ""


# -------------------------
# 사용자 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]):
    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 or 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=0o0640)


# -------------------------
# 대표 공유폴더
# -------------------------
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 "")
        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:
            return f.read().strip() or None
    except Exception:
        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):
    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):
    _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],
    create_folder: bool = False,   # ✅ 핵심: 기본은 폴더 생성 금지
) -> Tuple[bool, str]:
    """
    ✅ 규칙(share) 추가 정책
    - name: 표시용(폴더명 아님)
    - path: 실제 기존 폴더(기본). create_folder=True일 때만 생성 허용
    """
    name = (name or "").strip()
    raw_path = (path or "").strip()

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

    # path는 반드시 절대경로. (name이 잘못 들어오면 여기서 컷)
    if not raw_path.startswith("/"):
        return False, "경로는 반드시 /로 시작해야 합니다. (표시용 이름을 경로에 넣으면 안 됩니다)"

    path = os.path.abspath(raw_path)
    shares = list_share()

    ok, why = _is_allowed_share_path(path)
    if not ok:
        return False, f"❌ 공유폴더 경로 정책 위반: {why}\n(허용: /mnt/storage 또는 /mnt/usb*)"

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

    # ✅ 1) 기본은 폴더 생성 금지: 없으면 에러
    if not os.path.exists(path):
        if not create_folder:
            return False, (
                "지정한 경로가 실제로 존재하지 않습니다.\n"
                "폴더를 만들고 싶다면 '폴더 생성' 옵션을 켠 뒤 다시 시도해야 합니다."
            )
        # ✅ 2) 옵션 켠 경우에만 생성
        try:
            os.makedirs(path, exist_ok=True)
            set_owner_and_permission(path, perm=PERM_DIR)
        except Exception as e:
            return False, f"폴더 생성 오류: {e}"

    # 폴더는 있어야 함
    if not os.path.isdir(path):
        return False, "지정한 경로가 폴더가 아닙니다."

    # 권한 자동화(있는 폴더에도 적용)
    try:
        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()
    ok2, msg2 = sync_samba_conf()
    if not ok2:
        return True, f"공유폴더 '{name}' 추가됨. (단, Samba 반영 실패)\n{msg2}"

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


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

        # path 정책 재검증(파일이 누군가 손댐 대비)
        ok, _ = _is_allowed_share_path(share_path)
        if not ok:
            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는 location별로 안전하게
    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:
            print(r.stdout, r.stderr)
            return False

        _sudo(["/usr/sbin/nginx", "-s", "reload"])
        return True

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


# -------------------------
# ### Samba Sync ###
# -------------------------
def _write_samba_conf_text(shares: List[dict]) -> str:
    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

        ok, _ = _is_allowed_share_path(share_path)
        if not ok:
            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]:
    shares = load_shares()
    conf_text = _write_samba_conf_text(shares)

    _sudo(["cp", "-a", SAMBA_CONF_PATH, SAMBA_CONF_PATH + ".bak"])

    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}"

    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()

    r1 = _sudo(["systemctl", "restart", "smbd"])
    if r1.returncode != 0:
        return False, f"smbd restart failed:\n{(r1.stdout or '')}\n{(r1.stderr or '')}".strip()

    # nmbd는 환경 따라 없을 수 있음 → 실패해도 치명 아님
    _sudo(["systemctl", "restart", "nmbd"])

    return True, "Samba 설정 동기화 및 서비스 재시작 완료"


# -------------------------
# 공유폴더 후보 목록(대표폴더 하위)
# -------------------------
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


# -------------------------
# 파일/폴더 생성 안전 도우미 (✅ 777 금지로 수정)
# -------------------------
def safe_create_folder(path: str):
    os.makedirs(path, exist_ok=True)
    set_owner_and_permission(path, perm=PERM_DIR)


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


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


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=PERM_DIR)
        for f in files:
            set_owner_and_permission(os.path.join(root, f), perm=PERM_FILE)
