# 경로/파일: realcom-nas-ui/modules/backup.py
# 목적: (기존 rsync 백업 정책) -> (Timeshift 스냅샷 정책)으로 전환
# 적용: 이 파일 전체를 통째로 교체
# Ctrl+F: POLICY_FILE / run_policy / add_cron_job / realnas-timeshift

import os
import json
import datetime
import uuid
import subprocess
import re
from typing import List, Dict, Optional

# =========================
# PATH POLICY (app.py/yml과 일치)
# =========================
POLICY_FILE = "/opt/realcom-nas/config/timeshift_policies.json"
LOG_DIR = "/opt/realcom-nas/backup_logs"  # 기존 경로 재사용 (UI 연동 깨지지 않게)
RUNNER_SCRIPT = "/opt/realcom-nas/realcom-nas-ui/modules/run_timeshift_policy.py"

# Timeshift 래퍼 (yml에서 만들어둔 것)
TIMESHIFT_WRAPPER = "/usr/local/sbin/realnas-timeshift"

# Cron comment prefix
CRON_COMMENT_PREFIX = "# TimeshiftPolicyID:"


# =========================
# 디렉토리 보장
# =========================
def _ensure_dirs() -> None:
    os.makedirs(os.path.dirname(POLICY_FILE), exist_ok=True)

    try:
        os.makedirs(LOG_DIR, exist_ok=True)
    except PermissionError:
        # 최후의 폴백 (로그만 임시 위치 사용)
        fallback = "/tmp/realnas_backup_logs"
        os.makedirs(fallback, exist_ok=True)


_ensure_dirs()


# =========================
# 정책 I/O
# =========================
def load() -> List[Dict]:
    """정책 목록 로드"""
    if not os.path.exists(POLICY_FILE):
        return []
    try:
        with open(POLICY_FILE, "r", encoding="utf-8") as f:
            data = json.load(f)
            if isinstance(data, list):
                return data
            return []
    except Exception:
        return []


def save(policies: List[Dict]) -> None:
    """정책 목록 저장"""
    with open(POLICY_FILE, "w", encoding="utf-8") as f:
        json.dump(policies, f, ensure_ascii=False, indent=2)


def list_policies() -> List[Dict]:
    return load()


def get_policy(pid: str) -> Optional[Dict]:
    """단일 정책 반환"""
    policies = load()
    return next((x for x in policies if x.get("id") == pid), None)


# =========================
# Timeshift 실행/조회
# =========================
def _now() -> str:
    return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")


def _log_path(pid: str) -> str:
    return os.path.join(LOG_DIR, f"{pid}.log")


def _append_log(pid: str, msg: str) -> None:
    logf = _log_path(pid)
    with open(logf, "a", encoding="utf-8") as f:
        f.write(msg.rstrip() + "\n")


def _run_cmd(cmd: List[str], timeout: int = 3600) -> subprocess.CompletedProcess:
    """
    표준 실행 헬퍼.
    - 래퍼는 sudo 권한 필요할 수 있음. (sudoers에서 realnas-timeshift 허용)
    """
    return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)


def timeshift_status() -> str:
    """timeshift 상태(check)"""
    cmd = ["sudo", TIMESHIFT_WRAPPER, "status"]
    try:
        p = _run_cmd(cmd, timeout=60)
        if p.returncode == 0:
            return "OK"
        return f"FAILED({p.returncode})"
    except Exception:
        return "UNKNOWN"


def list_snapshots_raw() -> str:
    """timeshift --list 출력 원문"""
    cmd = ["sudo", TIMESHIFT_WRAPPER, "list"]
    p = _run_cmd(cmd, timeout=120)
    out = (p.stdout or "") + ("\n" + p.stderr if p.stderr else "")
    return out.strip()


def parse_snapshots(list_output: str) -> List[Dict]:
    """
    timeshift --list 출력은 배포판/버전에 따라 포맷이 약간 다름.
    여기선 “안전하게” 최소한의 정보만 뽑는다.
    """
    snaps: List[Dict] = []
    lines = [ln.strip() for ln in (list_output or "").splitlines() if ln.strip()]

    # 일반적으로 snapshot name이 포함된 라인들을 대충 잡아냄
    # 예: "Name: 2025-12-28_10-30-01" 같은 패턴 또는 날짜_시간 문자열
    name_pat = re.compile(r"(\d{4}-\d{2}-\d{2}[_\s]\d{2}-\d{2}-\d{2})")

    for ln in lines:
        m = name_pat.search(ln)
        if m:
            snaps.append({"name": m.group(1), "raw": ln})
    # 중복 제거
    uniq = {}
    for s in snaps:
        uniq[s["name"]] = s
    return list(uniq.values())


# =========================
# 정책 CRUD
# =========================
def add_policy(
    name: str,
    schedule: str,
    comment: str = "RealNAS UI",
    keep_last: int = 0,
    schedule_str: str = ""
) -> str:
    """
    Timeshift 정책 추가
    - schedule: cron 표현식 ("0 3 * * *" 등). 비우면 스케줄 없음.
    - comment: timeshift create 시 comments로 들어감
    - keep_last: 0이면 자동정리 안 함. 1 이상이면 최근 N개만 남기고 삭제 시도.
    """
    policies = load()
    pid = str(uuid.uuid4())[:8]
    policies.append({
        "id": pid,
        "name": name,
        "type": "timeshift",
        "comment": comment,
        "keep_last": int(keep_last) if str(keep_last).isdigit() else 0,
        "schedule": schedule,
        "schedule_str": schedule_str
    })
    save(policies)
    add_cron_job(pid, schedule)
    return pid


def update_policy(
    pid: str,
    name: Optional[str] = None,
    schedule: Optional[str] = None,
    comment: Optional[str] = None,
    keep_last: Optional[int] = None,
    schedule_str: Optional[str] = None
) -> bool:
    """정책 수정"""
    policies = load()
    updated = False
    for p in policies:
        if p.get("id") == pid:
            if name is not None:
                p["name"] = name
            if comment is not None:
                p["comment"] = comment
            if keep_last is not None:
                p["keep_last"] = int(keep_last) if str(keep_last).isdigit() else 0
            if schedule_str is not None:
                p["schedule_str"] = schedule_str
            if schedule is not None:
                p["schedule"] = schedule
                update_cron_job(pid, schedule)
            updated = True
            break
    if updated:
        save(policies)
        return True
    return False


def remove_policy(pid: str) -> None:
    """정책 삭제 (+cron, 로그 삭제)"""
    policies = load()
    policies = [p for p in policies if p.get("id") != pid]
    save(policies)
    remove_cron_job(pid)

    logf = _log_path(pid)
    if os.path.exists(logf):
        try:
            os.remove(logf)
        except Exception:
            pass


# =========================
# 정책 실행 (Timeshift 스냅샷 생성 + 선택적 정리)
# =========================
def run_policy(pid: str) -> str:
    """
    정책 1회 실행
    - timeshift create
    - keep_last > 0이면 list 후 오래된 스냅샷 삭제 시도
    """
    p = get_policy(pid)
    if not p:
        return "정책을 찾을 수 없음"

    t = _now()
    _append_log(pid, f"[{t}] START policy={pid} name={p.get('name','')}")

    # 1) timeshift create
    comment = (p.get("comment") or "RealNAS UI").strip()
    cmd = ["sudo", TIMESHIFT_WRAPPER, "create"]
    # 래퍼가 comments 고정이면 여기선 메시지만 남김
    try:
        proc = _run_cmd(cmd, timeout=7200)
        _append_log(pid, f"[{t}] CMD: {' '.join(cmd)}")
        _append_log(pid, f"[{t}] RET={proc.returncode}")
        if proc.stdout:
            _append_log(pid, f"[{t}] STDOUT:\n{proc.stdout}")
        if proc.stderr:
            _append_log(pid, f"[{t}] STDERR:\n{proc.stderr}")

        if proc.returncode != 0:
            _append_log(pid, f"[{t}] FAIL create snapshot")
            return f"스냅샷 생성 실패 (code {proc.returncode})"

    except Exception as e:
        _append_log(pid, f"[{t}] EXCEPTION: {e}")
        return "오류 발생"

    # 2) optional retention: keep_last
    keep_last = int(p.get("keep_last") or 0)
    if keep_last > 0:
        try:
            raw = list_snapshots_raw()
            snaps = parse_snapshots(raw)

            # 최신순 정렬(이름이 YYYY-MM-DD_HH-MM-SS라 lexicographic 정렬이 시간순이 됨)
            snaps_sorted = sorted(snaps, key=lambda x: x["name"], reverse=True)
            to_keep = snaps_sorted[:keep_last]
            to_delete = snaps_sorted[keep_last:]

            _append_log(pid, f"[{_now()}] RETENTION keep_last={keep_last} total={len(snaps_sorted)} delete={len(to_delete)}")

            for s in to_delete:
                snap_name = s["name"]
                del_cmd = ["sudo", TIMESHIFT_WRAPPER, "delete", snap_name]
                dp = _run_cmd(del_cmd, timeout=1800)
                _append_log(pid, f"[{_now()}] DEL {snap_name} RET={dp.returncode}")
                if dp.stdout:
                    _append_log(pid, dp.stdout)
                if dp.stderr:
                    _append_log(pid, dp.stderr)

        except Exception as e:
            _append_log(pid, f"[{_now()}] RETENTION ERROR: {e}")
            # retention 실패해도 create는 성공이니 성공 반환

    _append_log(pid, f"[{_now()}] DONE OK")
    return "스냅샷 생성 완료"


def get_last_log(pid: str) -> Optional[str]:
    """최근 로그 한 줄 반환"""
    logf = _log_path(pid)
    if not os.path.exists(logf):
        return None
    try:
        with open(logf, "r", encoding="utf-8") as f:
            lines = f.readlines()
        for line in reversed(lines):
            l = line.strip()
            if l:
                return l
    except Exception:
        return None
    return None


# =========================
# ---- 크론탭 관리 관련 ----
# =========================
def list_cron_jobs() -> Dict[str, str]:
    """
    realnas 유저 크론에 등록된 timeshift 정책 스케줄 조회
    """
    jobs: Dict[str, str] = {}
    try:
        crontab = subprocess.check_output(["sudo", "-u", "realnas", "crontab", "-l"], text=True)
        for line in crontab.splitlines():
            if CRON_COMMENT_PREFIX in line:
                m = re.search(r"# TimeshiftPolicyID:(\w{8}) schedule:(.+)", line)
                if m:
                    pid = m.group(1)
                    sched = m.group(2)
                    jobs[pid] = sched
    except subprocess.CalledProcessError:
        pass
    except Exception:
        pass
    return jobs


def add_cron_job(pid: str, schedule: str) -> bool:
    """
    schedule이 비어있으면 cron 제거.
    크론 실행은 RUNNER_SCRIPT 고정 경로 사용(상대경로 금지).
    """
    schedule = (schedule or "").strip()
    if not schedule:
        remove_cron_job(pid)
        return True

    jobs = list_cron_jobs()

    # ✅ 절대경로로 고정
    cron_cmd = f"/usr/bin/python3 {RUNNER_SCRIPT} --id={pid}"
    cron_line = f"{schedule} {cron_cmd} {CRON_COMMENT_PREFIX}{pid} schedule:{schedule}"

    if pid in jobs and jobs[pid] == schedule:
        return True
    if pid in jobs and jobs[pid] != schedule:
        remove_cron_job(pid)

    try:
        current = subprocess.check_output(["sudo", "-u", "realnas", "crontab", "-l"], text=True).splitlines()
    except subprocess.CalledProcessError:
        current = []
    except Exception:
        current = []

    current.append(cron_line)
    new_crontab = "\n".join(current).rstrip() + "\n"
    proc = subprocess.run(["sudo", "-u", "realnas", "crontab"], input=new_crontab, text=True)
    return proc.returncode == 0


def remove_cron_job(pid: str) -> None:
    try:
        current = subprocess.check_output(["sudo", "-u", "realnas", "crontab", "-l"], text=True).splitlines()
    except subprocess.CalledProcessError:
        return
    except Exception:
        return

    new_crontab = [line for line in current if f"{CRON_COMMENT_PREFIX}{pid}" not in line]
    new_crontab_str = "\n".join(new_crontab).rstrip() + "\n"
    subprocess.run(["sudo", "-u", "realnas", "crontab"], input=new_crontab_str, text=True)


def update_cron_job(pid: str, schedule: str) -> bool:
    remove_cron_job(pid)
    return add_cron_job(pid, schedule)
