#!/usr/bin/env bash
# 数据库分库备份脚本（本地 + S3 兼容对象存储）
# 用法：backup.sh [assets|metrics|all]
#   assets  -> 备份 assets.db，保留 14 份（建议每日）
#   metrics -> 备份 metrics.db，保留 4 份（建议每周）
#   all     -> 两者都备份
#
# S3 上传（可选）：配置 .env 中 S3_* 项后，备份文件自动上传到
# 任意 S3 兼容对象存储（Cloudflare R2 / MinIO / 阿里云 OSS / 腾讯云 COS 等），
# 远端保留份数与本地一致。未配置 S3 时仅本地备份（向后兼容）。
# 实现为纯 Python 标准库（AWS SigV4 签名），零额外依赖。
set -euo pipefail

APP_DIR="${APP_DIR:-/opt/vps-manager}"
cd "$APP_DIR"

TARGET="${1:-all}"

python3 - "$TARGET" <<'PYEOF'
import hashlib
import hmac
import os
import sqlite3
import sys
import urllib.error
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
from datetime import datetime, timezone
from pathlib import Path

target = sys.argv[1]
data_dir = Path("data")
backup_root = data_dir / "backups"
ts = datetime.now().strftime("%Y%m%d_%H%M%S")

# ---------------- 本地备份 ----------------

def backup(db_name: str, keep: int) -> Path | None:
    src = data_dir / db_name
    stem = db_name.replace(".db", "")
    if not src.exists():
        print(f"[skip] {db_name} 不存在")
        return None
    out_dir = backup_root / stem
    out_dir.mkdir(parents=True, exist_ok=True)
    dst = out_dir / f"{stem}_{ts}.db"

    con = sqlite3.connect(str(src))
    bkp = sqlite3.connect(str(dst))
    con.backup(bkp)
    bkp.close()
    con.close()

    files = sorted(
        out_dir.glob(f"{stem}_*.db"),
        key=lambda p: p.stat().st_mtime,
        reverse=True,
    )
    for old in files[keep:]:
        old.unlink()
    print(f"[ok] {db_name} -> {dst}（保留 {keep} 份）")
    return dst

# ---------------- S3 上传（纯标准库 SigV4） ----------------

def load_env(path: Path) -> dict:
    """极简 .env 解析（KEY=VALUE，忽略注释/空行），os.environ 优先"""
    env = {}
    if path.exists():
        for line in path.read_text().splitlines():
            line = line.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            k, v = line.split("=", 1)
            env[k.strip()] = v.strip().strip('"').strip("'")
    for k, v in os.environ.items():
        if v:
            env[k] = v
    return env


def _sign(key: bytes, msg: str) -> bytes:
    return hmac.new(key, msg.encode(), hashlib.sha256).digest()


def _sig_key(secret: str, date: str, region: str, service: str) -> bytes:
    k = _sign(f"AWS4{secret}".encode(), date)
    k = _sign(k, region)
    k = _sign(k, service)
    return _sign(k, "aws4_request")


def _s3_request(endpoint: str, access: str, secret: str, region: str,
                method: str, bucket: str, key: str, query: str,
                body: bytes, content_type: str = "application/octet-stream") -> bytes:
    """S3 API 请求（path-style），返回响应体"""
    now = datetime.now(timezone.utc)
    amz_date = now.strftime("%Y%m%dT%H%M%SZ")
    date = now.strftime("%Y%m%d")
    payload_hash = hashlib.sha256(body).hexdigest()
    host = urllib.parse.urlparse(endpoint).netloc
    q = urllib.parse.quote(key, safe="/")

    canonical_uri = f"/{bucket}/{q}"
    canonical_query = query  # 已按 RFC3986 编码
    canonical_headers = (
        f"host:{host}\n"
        f"x-amz-content-sha256:{payload_hash}\n"
        f"x-amz-date:{amz_date}\n"
    )
    signed_headers = "host;x-amz-content-sha256;x-amz-date"
    canonical_request = "\n".join(
        [method, canonical_uri, canonical_query, canonical_headers, signed_headers, payload_hash]
    )
    scope = f"{date}/{region}/s3/aws4_request"
    string_to_sign = "\n".join(
        ["AWS4-HMAC-SHA256", amz_date, scope,
         hashlib.sha256(canonical_request.encode()).hexdigest()]
    )
    signature = hmac.new(
        _sig_key(secret, date, region, "s3"), string_to_sign.encode(), hashlib.sha256
    ).hexdigest()
    auth = (
        f"AWS4-HMAC-SHA256 Credential={access}/{scope}, "
        f"SignedHeaders={signed_headers}, Signature={signature}"
    )

    url = f"{endpoint.rstrip('/')}/{bucket}/{q}"
    if query:
        url += "?" + query
    req = urllib.request.Request(
        url, data=body, method=method,
        headers={
            "Authorization": auth,
            "x-amz-content-sha256": payload_hash,
            "x-amz-date": amz_date,
            "Content-Type": content_type,
        },
    )
    try:
        with urllib.request.urlopen(req, timeout=90) as resp:
            return resp.read()
    except urllib.error.HTTPError as e:
        raise RuntimeError(f"S3 {method} {key} 失败: HTTP {e.code} {e.read().decode(errors='replace')[:300]}")


def s3_upload(env: dict, dst: Path, stem: str, keep: int) -> None:
    """上传单个备份文件到 S3，并清理远端过期备份（保留 keep 份）"""
    endpoint = env.get("S3_ENDPOINT_URL", "")
    access = env.get("S3_ACCESS_KEY", "")
    secret = env.get("S3_SECRET_KEY", "")
    bucket = env.get("S3_BUCKET", "")
    if not (endpoint and access and secret and bucket):
        print("[s3] 未配置 S3_*（ENDPOINT/ACCESS_KEY/SECRET_KEY/BUCKET），跳过远端备份")
        return
    region = env.get("S3_REGION", "auto")
    prefix = env.get("S3_PREFIX", "vps-manager").rstrip("/")
    key = f"{prefix}/{stem}/{dst.name}"

    body = dst.read_bytes()
    _s3_request(endpoint, access, secret, region, "PUT", bucket, key, "", body)
    print(f"[s3] {dst.name} -> {endpoint}/{bucket}/{key}")

    # 列出同目录下已有备份，按 key 排序（时间戳命名即字典序），清理过期对象
    # 注意：prefix 必须带参数名且值按 RFC3986 编码（含 / -> %2F），否则会列出全桶并误删其他目录
    list_query = "list-type=2&prefix=" + urllib.parse.quote(f"{prefix}/{stem}/", safe="")
    resp = _s3_request(endpoint, access, secret, region, "GET", bucket, "", list_query, b"")
    root = ET.fromstring(resp)
    ns = {"s3": "http://s3.amazonaws.com/doc/2006-03-01/"}
    keys = [e.text for e in root.findall(".//s3:Key", ns) if e.text]
    keys.sort(reverse=True)
    for old_key in keys[keep:]:
        _s3_request(endpoint, access, secret, region, "DELETE", bucket, old_key, "", b"")
        print(f"[s3] 清理过期备份: {old_key}")


def run(db_name: str, keep: int) -> None:
    dst = backup(db_name, keep)
    if dst is None:
        return
    try:
        env = load_env(Path(".env"))
        s3_upload(env, dst, db_name.replace(".db", ""), keep)
    except Exception as e:
        # 本地备份已成功；S3 失败需要显式暴露（systemd 会标记 failed）
        print(f"[s3][error] {e}", file=sys.stderr)
        sys.exit(1)


if target in ("assets", "all"):
    run("assets.db", 14)
if target in ("metrics", "all"):
    run("metrics.db", 4)
PYEOF
