#!/usr/bin/env python3
"""backup.py — Creates a BACKUP of state + history (insurance against volume loss / down -v).

What it backs up:
  1) Trade/equity/user history DB:
       - SQLite (sqlite:///state/coinbot.db) -> consistent ONLINE copy (sqlite3
         .backup API; stays intact even while the bot is WRITING). Not 'cp',
         because cp may copy a half-written file during an open WAL/write.
       - PostgreSQL -> pg_dump (requires pg_dump on PATH; skipped with a warning otherwise).
  2) Open position ledger + cost basis + OCO ids: state/portfolio.*.json
     (cost basis in portfolio.to_dict, oco_order_id inside risk.Position).

Output: files under <outdir>/brimoza-backup-YYYYmmddTHHMMSSZ/.
Retention: --keep N keeps the N most recent backups and deletes older ones (0 = unlimited).

WHY: There was no backup/dump/cron anywhere in the repo (verified with grep). Volume
corruption, an accidental 'docker compose down -v', a full disk, or file
corruption would irreversibly lose ALL history + the open position ledger
(untracked real-money risk). This script closes that gap; it should be invoked
periodically via a cron/timer (see README).

Example:
  python scripts/backup.py                          # backup under ./backups
  python scripts/backup.py --outdir /srv/backups --keep 14
  # cron (host): 0 * * * * cd /app && python scripts/backup.py --outdir /srv/backups --keep 48
"""
from __future__ import annotations

import argparse
import datetime
import os
import pathlib
import shutil
import sqlite3
import subprocess
import sys

sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))

try:
    from dotenv import load_dotenv

    load_dotenv(pathlib.Path(__file__).resolve().parent.parent / ".env")
except Exception:  # noqa: BLE001  (if dotenv is missing, env may already be populated)
    pass

REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
DEFAULT_SQLITE_URL = "sqlite:///state/coinbot.db"


def _utc_stamp() -> str:
    return datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ")


def _backup_sqlite(src_path: pathlib.Path, dest: pathlib.Path) -> tuple[bool, str]:
    """Copies the DB consistently EVEN while open (online backup API). NOT cp."""
    if not src_path.exists():
        return False, f"SQLite dosyasi yok: {src_path} (henuz islem yok olabilir)"
    try:
        # connect read-only; do not block live writes. backup() gives a consistent copy including WAL.
        src = sqlite3.connect(f"file:{src_path}?mode=ro", uri=True)
        try:
            out = sqlite3.connect(str(dest))
            try:
                src.backup(out)
            finally:
                out.close()
        finally:
            src.close()
        return True, f"SQLite yedegi: {dest.name} ({dest.stat().st_size} bayt)"
    except Exception as exc:  # noqa: BLE001
        return False, f"SQLite yedegi BASARISIZ: {exc}"


def _backup_postgres(url: str, dest: pathlib.Path) -> tuple[bool, str]:
    """Logical backup via pg_dump. Skip if pg_dump is missing (warning, not error)."""
    if shutil.which("pg_dump") is None:
        return False, "pg_dump PATH'te yok; PostgreSQL yedegi ATLANDI (postgresql-client kurun)"
    # Convert the SQLAlchemy URL (postgresql+psycopg2://...) into the form pg_dump understands.
    pg_url = url.replace("postgresql+psycopg2://", "postgresql://").replace(
        "postgres+psycopg2://", "postgresql://"
    )
    try:
        with open(dest, "wb") as fh:
            proc = subprocess.run(
                ["pg_dump", "--no-owner", "--no-privileges", pg_url],
                stdout=fh,
                stderr=subprocess.PIPE,
                check=False,
            )
        if proc.returncode != 0:
            err = proc.stderr.decode("utf-8", "replace").strip()
            return False, f"pg_dump BASARISIZ (rc={proc.returncode}): {err[:300]}"
        return True, f"PostgreSQL yedegi: {dest.name} ({dest.stat().st_size} bayt)"
    except Exception as exc:  # noqa: BLE001
        return False, f"pg_dump BASARISIZ: {exc}"


def _backup_json_state(state_dir: pathlib.Path, snap: pathlib.Path) -> list[str]:
    """Copy portfolio.*.json (open position ledger + cost basis + OCO id)."""
    msgs: list[str] = []
    if not state_dir.exists():
        return [f"state/ klasoru yok: {state_dir}"]
    files = sorted(state_dir.glob("portfolio.*.json"))
    if not files:
        msgs.append("portfolio.*.json bulunamadi (henuz durum yazilmamis olabilir)")
    for f in files:
        try:
            shutil.copy2(f, snap / f.name)
            msgs.append(f"JSON durum yedegi: {f.name} ({f.stat().st_size} bayt)")
        except Exception as exc:  # noqa: BLE001
            msgs.append(f"JSON durum yedegi BASARISIZ ({f.name}): {exc}")
    return msgs


def _prune(outdir: pathlib.Path, keep: int) -> list[str]:
    if keep <= 0:
        return []
    snaps = sorted(
        (p for p in outdir.glob("brimoza-backup-*") if p.is_dir()),
        key=lambda p: p.name,
        reverse=True,
    )
    removed: list[str] = []
    for old in snaps[keep:]:
        try:
            shutil.rmtree(old)
            removed.append(f"eski yedek silindi: {old.name}")
        except Exception as exc:  # noqa: BLE001
            removed.append(f"eski yedek SILINEMEDI ({old.name}): {exc}")
    return removed


def main(argv: list[str] | None = None) -> int:
    p = argparse.ArgumentParser(
        prog="backup.py",
        description="Durum (JSON) + gecmis (SQLite/PostgreSQL) yedegi alir.",
    )
    p.add_argument(
        "--outdir",
        default=str(REPO_ROOT / "backups"),
        help="Yedeklerin yazilacagi dizin (varsayilan: ./backups). Volume DISINDA bir yol secin!",
    )
    p.add_argument(
        "--keep",
        type=int,
        default=0,
        help="En yeni N yedegi tut, gerisini sil (0 = sinirsiz, varsayilan).",
    )
    args = p.parse_args(argv)

    outdir = pathlib.Path(args.outdir)
    outdir.mkdir(parents=True, exist_ok=True)
    snap = outdir / f"brimoza-backup-{_utc_stamp()}"
    snap.mkdir(parents=True, exist_ok=False)

    url = os.environ.get("DATABASE_URL") or DEFAULT_SQLITE_URL
    results: list[str] = []
    ok_db = False

    if url.startswith("sqlite:///"):
        rel = url[len("sqlite:///"):]
        src = pathlib.Path(rel)
        if not src.is_absolute():
            src = REPO_ROOT / rel
        ok_db, msg = _backup_sqlite(src, snap / "coinbot.db")
        results.append(msg)
    elif url.startswith(("postgresql", "postgres")):
        ok_db, msg = _backup_postgres(url, snap / "brimoza.sql")
        results.append(msg)
    else:
        results.append(f"Bilinmeyen DATABASE_URL semasi, DB yedegi atlandi: {url.split('://')[0]}")

    results.extend(_backup_json_state(REPO_ROOT / "state", snap))
    results.extend(_prune(outdir, args.keep))

    print(f"Yedek dizini: {snap}")
    for m in results:
        print(f"  - {m}")

    # The JSON state backup is evaluated separately from the DB backup; if neither
    # exists, do not leave an empty snapshot.
    produced = any(snap.iterdir())
    if not produced:
        try:
            snap.rmdir()
        except OSError:
            pass
        print("UYARI: yedeklenecek hicbir sey bulunamadi (DB/JSON yok).")
        return 1
    # If the DB backup failed (but JSON exists), still warn with exit 1 so it shows in cron logs.
    return 0 if ok_db else 1


if __name__ == "__main__":
    raise SystemExit(main())
