# 경로/파일: realcom-nas-ui/modules/disk.py
# 목적: 디스크/파티션 + 사용량(df) + 온도/SMART(smartctl) +
#      ✅ elFinder 등록상태/허용루트(/mnt/storage,/mnt/usb,/mnt/hdd,/mnt/nvme)/OS 루트 디스크 표시
#      ✅ (추가) 디스크 모델/벤더/전송(TRAN)/시리얼 표시용 model_info 제공
# 주의: ❌ Flask 라우트/ app 참조 금지 (app.py에서 라우팅)
# 붙여넣기: 이 파일 전체 교체

from __future__ import annotations

import json
import os
import re
import shutil
import subprocess
from typing import Any, Dict, List, Tuple

SMARTCTL_CANDIDATES = ["/usr/sbin/smartctl", "/sbin/smartctl", "smartctl"]

# elFinder 설정 우선순위 (connector.minimal.php와 동일)
ELFINDER_PRIMARY = "/etc/realnas/elfinder.json"
ELFINDER_FALLBACK = "/opt/realcom-nas/config/elfinder.json"


def _is_exec_file(p: str) -> bool:
    return os.path.exists(p) and os.path.isfile(p) and os.access(p, os.X_OK)


def _which_smartctl() -> str:
    for p in SMARTCTL_CANDIDATES:
        if p == "smartctl":
            w = shutil.which("smartctl")
            if w:
                return w
        else:
            if _is_exec_file(p):
                return p
    return "smartctl"


SMARTCTL = _which_smartctl()


def _run(cmd: List[str], timeout: int = 6) -> Tuple[int, str]:
    """stdout+stderr 합쳐서 반환"""
    try:
        p = subprocess.run(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            timeout=timeout,
            check=False,
        )
        return p.returncode, (p.stdout or "")
    except subprocess.TimeoutExpired:
        return 124, "timeout"
    except Exception as e:
        return 1, f"error: {e}"


def _run_smartctl(args: List[str], timeout: int = 6) -> Tuple[int, str]:
    """
    smartctl 권한 이슈 방어:
    1) smartctl 직접
    2) 권한 문제면 sudo -n smartctl 재시도 (비번 요구하면 즉시 실패)
    """
    rc, out = _run([SMARTCTL] + args, timeout=timeout)
    if rc == 0:
        return rc, out

    lower = (out or "").lower()
    if ("permission denied" in lower) or ("requires root" in lower) or ("operation not permitted" in lower):
        rc2, out2 = _run(["sudo", "-n", SMARTCTL] + args, timeout=timeout)
        return rc2, out2

    return rc, out


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


# ============================================================
# ✅ RealNAS 마운트 루트 정책 (최종)
# - 메인: /mnt/storage
# - 외장/추가: /mnt/usb, /mnt/hdd, /mnt/nvme  (버킷)
# - 실제 마운트: /mnt/usb/<name>, /mnt/hdd/<name>, /mnt/nvme/<name>
# - OS 루트(/), /media/* 등은 절대 허용하지 않음
# ============================================================
_FIXED_ALLOWED_ROOTS = ["/mnt/storage", "/mnt/usb", "/mnt/hdd", "/mnt/nvme", "/mnt/tmp"]


def _allowed_roots_runtime() -> List[str]:
    roots: List[str] = []
    for p in _FIXED_ALLOWED_ROOTS:
        if os.path.isdir(p):
            roots.append(p)
    return sorted(set(roots))


def _is_under_allowed_roots(path: str) -> bool:
    """
    path가 허용 루트 아래인지.
    ✅ 보안: prefix 오탐(/mnt/storage2 같은 케이스) 방지 위해 경계 체크
    """
    if not path:
        return False

    rp = _realpath(path)
    if not rp or rp == "/":
        return False

    rp = rp.rstrip("/")
    for ar in _allowed_roots_runtime():
        rr = _realpath(ar)
        if not rr:
            continue
        rr = rr.rstrip("/")
        if rp == rr or rp.startswith(rr + "/"):
            return True
    return False


def _load_elfinder_roots() -> Dict[str, str]:
    """
    elfinder.json의 roots를 path(realpath) -> alias 로 맵핑
    """
    cfg_path = ELFINDER_PRIMARY if os.path.exists(ELFINDER_PRIMARY) else ELFINDER_FALLBACK
    try:
        raw = ""
        if os.path.exists(cfg_path):
            with open(cfg_path, "r", encoding="utf-8") as f:
                raw = f.read()
        data = json.loads(raw) if raw.strip() else {}
    except Exception:
        data = {}

    roots = data.get("roots") if isinstance(data, dict) else None
    if not isinstance(roots, list):
        return {}

    out: Dict[str, str] = {}
    for r in roots:
        if not isinstance(r, dict):
            continue

        p = str(r.get("path") or "").strip()
        if not p:
            continue
        alias = str(r.get("alias") or "").strip()

        # ✅ 허용 루트 밖은 무시
        if not _is_under_allowed_roots(p):
            continue

        rp = _realpath(p)
        if not rp:
            continue

        out[rp] = alias or os.path.basename(rp.rstrip("/"))
    return out


def _get_root_mount_source() -> str:
    rc, out = _run(["findmnt", "-n", "-o", "SOURCE", "/"], timeout=4)
    return (out or "").strip() if rc == 0 else ""


def _disk_from_partition(dev: str) -> str:
    dev = (dev or "").strip()
    if not dev.startswith("/dev/"):
        return ""

    if re.match(r"^/dev/nvme\d+n\d+p\d+$", dev):
        return re.sub(r"p\d+$", "", dev)

    if re.match(r"^/dev/[a-z]+[0-9]+$", dev):
        return re.sub(r"[0-9]+$", "", dev)

    return dev


def get_disk_temperature(dev_path: str):
    dev_path = (dev_path or "").strip()
    if not dev_path.startswith("/dev/"):
        return "N/A"

    if "nvme" in dev_path:
        rc, out = _run_smartctl(["-a", dev_path], timeout=6)
        if rc == 0 and out:
            m = re.search(r"Temperature:\s+(\d+)\s+Celsius", out, re.I)
            if m:
                v = int(m.group(1))
                if 0 < v < 110:
                    return v
            m2 = re.search(r"Temperature Sensor \d+:\s+(\d+)\s*C", out, re.I)
            if m2:
                v = int(m2.group(1))
                if 0 < v < 110:
                    return v

    rc, out = _run_smartctl(["-A", dev_path], timeout=6)
    if rc == 0 and out:
        for key in ("Temperature_Celsius", "Airflow_Temperature_Cel"):
            m = re.search(rf"^\s*\d+\s+{key}\s+.*$", out, re.I | re.M)
            if m:
                line = m.group(0)
                nums = re.findall(r"\b(\d{1,3})\b", line)
                for n in reversed(nums):
                    v = int(n)
                    if 0 < v < 110:
                        return v

        for m in re.finditer(r"^\s*(190|194)\s+.+$", out, re.M):
            line = m.group(0)
            nums = re.findall(r"\b(\d{1,3})\b", line)
            for n in reversed(nums):
                v = int(n)
                if 0 < v < 110:
                    return v

        for line in out.splitlines():
            if re.search(r"Temperature", line, re.I):
                nums = re.findall(r"\b(\d{1,3})\b", line)
                for n in nums:
                    v = int(n)
                    if 0 < v < 110:
                        return v

    return "N/A"


def get_disk_smart_status(dev_path: str) -> str:
    dev_path = (dev_path or "").strip()
    if not dev_path.startswith("/dev/"):
        return "N/A"

    rc, out = _run_smartctl(["-H", dev_path], timeout=6)
    if rc != 0 and not out:
        return "N/A"

    if "PASSED" in out:
        return "정상"
    if "FAILED" in out:
        return "오류"

    lower = (out or "").lower()
    if "smart support is: unavailable" in lower or "smart disabled" in lower or "unknown usb bridge" in lower:
        return "지원안함"

    return "알수없음"


def _df_map() -> Dict[str, Dict[str, str]]:
    rc, out = _run(["df", "-h", "--output=source,size,used,avail,pcent,target"], timeout=6)
    if rc != 0 or not out:
        return {}

    lines = out.strip().splitlines()
    if len(lines) <= 1:
        return {}

    mp: Dict[str, Dict[str, str]] = {}
    for line in lines[1:]:
        cols = line.split()
        if len(cols) != 6:
            continue
        src = cols[0].strip()
        if not src.startswith("/dev/"):
            continue
        key = src.replace("/dev/", "")
        mp[key] = {
            "size": cols[1],
            "used": cols[2],
            "avail": cols[3],
            "percent": cols[4],
            "mountpoint": cols[5],
        }
    return mp


def _suggest_alias_from_mount(mp: str) -> str:
    mp = (mp or "").strip().rstrip("/")
    if not mp:
        return ""

    if mp == "/mnt/storage":
        return "스토리지"
    if mp == "/mnt/usb":
        return "USB"
    if mp.startswith("/mnt/usb/"):
        return "USB-" + (mp.split("/")[-1] or "DATA")

    if mp == "/mnt/hdd":
        return "HDD"
    if mp.startswith("/mnt/hdd/"):
        return "HDD-" + (mp.split("/")[-1] or "DATA")

    if mp == "/mnt/nvme":
        return "NVMe"
    if mp.startswith("/mnt/nvme/"):
        return "NVMe-" + (mp.split("/")[-1] or "DATA")

    if mp == "/mnt/tmp":
        return "임시"

    return os.path.basename(mp) or "DATA"


def _build_model_info(vendor: str, model: str, size: str, tran: str = "", serial: str = "") -> str:
    vendor = (vendor or "").strip()
    model = (model or "").strip()
    size = (size or "").strip()
    serial = (serial or "").strip()

    left = " ".join([x for x in [vendor, model] if x]).strip()
    parts = [x for x in [left, size] if x]
    s = " · ".join(parts).strip()

    if serial:
        tail = serial[-4:] if len(serial) > 4 else serial
        s = f"{s} (#{tail})" if s else f"#{tail}"

    return s



def get_disks() -> List[Dict[str, Any]]:
    df_map = _df_map()
    elfinder_roots = _load_elfinder_roots()

    root_src = _get_root_mount_source()
    root_disk = _disk_from_partition(root_src)

    rc, out = _run(
        ["lsblk", "-J", "-o", "NAME,SIZE,FSTYPE,MOUNTPOINT,LABEL,TYPE,MODEL,SERIAL,VENDOR,TRAN"],
        timeout=6
    )
    if rc != 0 or not out.strip():
        return []

    try:
        data = json.loads(out)
    except Exception:
        return []

    disks: List[Dict[str, Any]] = []
    for dev in (data.get("blockdevices") or []):
        if not isinstance(dev, dict):
            continue

        name = str(dev.get("name") or "")
        dtype = str(dev.get("type") or "")
        if dtype != "disk":
            continue

        if not (name.startswith("nvme") or name.startswith("sd")):
            continue

        d_df = df_map.get(name, {})
        dev_path = f"/dev/{name}"

        temp = get_disk_temperature(dev_path)
        smart_status = get_disk_smart_status(dev_path)
        is_os_root_disk = (root_disk == dev_path) if (root_disk and dev_path) else False

        d_vendor = str(dev.get("vendor") or "")
        d_model = str(dev.get("model") or "")
        d_serial = str(dev.get("serial") or "")
        d_tran = str(dev.get("tran") or "")
        d_size = str(dev.get("size") or "")
        disk_model_info = _build_model_info(d_vendor, d_model, d_size, d_tran, d_serial)

        parts: List[Dict[str, Any]] = []
        rep_mount = ""

        for part in (dev.get("children") or []):
            if not isinstance(part, dict):
                continue

            pname = str(part.get("name") or "")
            p_df = df_map.get(pname, {})
            pm = (part.get("mountpoint") or p_df.get("mountpoint") or "").strip()

            if pm and not rep_mount:
                rep_mount = pm

            pm_real = _realpath(pm) if pm else ""
            allowed = _is_under_allowed_roots(pm) if pm else False

            registered = False
            alias = ""
            if pm_real and pm_real in elfinder_roots:
                registered = True
                alias = elfinder_roots.get(pm_real, "") or ""

            parts.append({
                "name": pname,
                "size": str(part.get("size") or ""),
                "fstype": str(part.get("fstype") or ""),
                "mountpoint": pm,
                "mountpoint_real": pm_real,
                "label": str(part.get("label") or ""),
                "type": str(part.get("type") or ""),
                "used": p_df.get("used", ""),
                "avail": p_df.get("avail", ""),
                "percent": p_df.get("percent", ""),
                "is_allowed_root": allowed,
                "is_elfinder_registered": registered,
                "elfinder_alias": alias,
                "suggested_alias": _suggest_alias_from_mount(pm) if pm else "",
                "model_info": disk_model_info,
            })

        disk_mp = (dev.get("mountpoint") or d_df.get("mountpoint") or rep_mount or "").strip()
        disk_mp_real = _realpath(disk_mp) if disk_mp else ""
        disk_allowed = _is_under_allowed_roots(disk_mp) if disk_mp else False
        disk_registered = bool(disk_mp_real and disk_mp_real in elfinder_roots)
        disk_alias = elfinder_roots.get(disk_mp_real, "") if disk_registered else ""

        disks.append({
            "name": name,
            "size": d_size,
            "fstype": str(dev.get("fstype") or ""),
            "mountpoint": disk_mp,
            "mountpoint_real": disk_mp_real,
            "label": str(dev.get("label") or ""),
            "type": dtype,
            "used": d_df.get("used", ""),
            "avail": d_df.get("avail", ""),
            "percent": d_df.get("percent", ""),
            "temperature": temp,
            "smart_status": smart_status,
            "parts": parts,
            "is_os_root_disk": is_os_root_disk,
            "is_allowed_root": disk_allowed,
            "is_elfinder_registered": disk_registered,
            "elfinder_alias": disk_alias,
            "suggested_alias": _suggest_alias_from_mount(disk_mp) if disk_mp else "",
            "vendor": d_vendor,
            "model": d_model,
            "serial": d_serial,
            "tran": d_tran,
            "model_info": disk_model_info,
        })

    return disks
