"""SSH-based file transfer for gnommo projects (commit / up / down).

Workflow:
  gnommo -p video1 commit -m "re-recorded slides 7-15"
  gnommo -p video1 up        # push to rendering server
  gnommo -p video1 down      # pull from rendering server

Design:
  - commit  appends a timestamped entry to commits.log
  - up      checks server commits.log for newer entry (aborts if found), then
            rsyncs the WHOLE project tree, excluding only the large/derived files
            in _SYNC_EXCLUDES. Everything else — manuscript, slides, narration
            raw_mov/, per-segment transcripts, events.json, audio, videos, keynote
            — is carried automatically, so a new kind of input file can never be
            silently left behind.
  - down    rsyncs the WHOLE project tree back, with the same _SYNC_EXCLUDES.

Sync model: move everything, exclude a small denylist. The exclusions are large
derived artifacts each side regenerates or ships on its own (rendered output,
preprocessed segments, downscales, chunk scratch), so they never travel over SSH.

Master = the machine's local tree. The PROJECT sync runs with rsync --delete, so a
file deleted locally and then `up`'d is removed on the server, and `down` mirrors the
server onto the local tree (deletions included). The Mac is the intended master: it
`up`s, the rig `down`s. Excluded paths are PROTECTED from --delete (rsync does not
touch excluded files on the receiver), so rig-only artifacts — above all the big
`*_processed.*` preprocess outputs — are never deleted by a mirror from the Mac.
Shared assets are NOT --deleted (a cross-project library; blind deletion could wipe
pexels downloaded on the other side), so that pass stays purely additive.
"""

import json
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Optional

_COMMITS_LOG = "commits.log"

# Files/dirs NEVER transferred in either direction. Everything else syncs, so new
# artifacts (transcripts, manuscript_transcribed.txt, events.json, scaffold.json, …)
# are carried automatically without touching this list. These are large, derived,
# or machine-local outputs that each side regenerates or ships itself:
#   - out/            rendered video (produced on the rig; `handoff` ships it for review)
#   - processed/      heavy preprocessed *_processed.mov segments (rig regenerates)
#   - intermediate/   ffmpeg work files
#   - low/ proxy/     derived downscales
#   - **/chunks/      per-chunk preprocess scratch
# Note: events.json / scaffold.json are deliberately NOT excluded — they must reach
# the rig so `down` + `render` is all that's needed there, and render never rewrites
# them, so they stay identical on both ends.
_SYNC_EXCLUDES = [
    "out/",
    "media/narration/processed/",
    "media/narration/old/",
    "media/narration/raw_mov/old/",
    "media/narration/intermediate/",
    "media/videos/intermediate/",
    "media/narration/low/",
    "media/videos/low/",
    "media/narration/proxy/",
    "media/videos/proxy/",
    "**/chunks/",
    "*_processed.*",  # preprocess outputs — rig-only, never transfer OR --delete them
    "*.tmp",
    ".*",       # rsync in-progress temp files (.filename.XXXXXX) and .DS_Store
]


# ---------------------------------------------------------------------------
# Shared assets
# ---------------------------------------------------------------------------

def _find_shared_assets_root(project_path: Path) -> Optional[Path]:
    """Return the local shared_assets directory, or None if not found."""
    candidate = project_path.parent / "shared_assets"
    if candidate.is_dir():
        return candidate
    try:
        from .cache import load_assets_config
        assets = load_assets_config()
        if assets:
            candidate = assets / "shared_assets"
            if candidate.is_dir():
                return candidate
    except Exception:
        pass
    return None


# ---------------------------------------------------------------------------
# Commit log helpers
# ---------------------------------------------------------------------------

def _read_log_lines(path: Path) -> list[str]:
    if not path.exists():
        return []
    return [l for l in path.read_text(encoding="utf-8").splitlines() if l.strip()]


def _last_timestamp(lines: list[str]) -> Optional[str]:
    for line in reversed(lines):
        ts = line.split(" | ")[0].strip()
        if ts:
            return ts
    return None


# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------

def cmd_commit(project_path: Path, message: str) -> int:
    log_path = project_path / _COMMITS_LOG
    ts = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
    with open(log_path, "a", encoding="utf-8") as f:
        f.write(f"{ts} | {message}\n")
    print(f"[{ts}] {message}")
    return 0


def cmd_up(project_path: Path, verbose: bool, dry_run: bool) -> int:
    from .cache import load_server_config

    server = load_server_config()
    if server is None:
        _print_server_error()
        return 1

    # Require a local commit
    log_path = project_path / _COMMITS_LOG
    local_lines = _read_log_lines(log_path)
    if not local_lines:
        print(f"Error: No commits. Run: gnommo -p {project_path.name} commit -m 'message'")
        return 1
    local_last = _last_timestamp(local_lines)

    # Fetch server's commits.log and compare
    remote_project = f"{server['path']}/{project_path.name}"
    ssh_cmd = ["ssh", "-p", server["port"], f"{server['user']}@{server['host']}"]
    result = subprocess.run(
        [*ssh_cmd, f"cat {remote_project}/{_COMMITS_LOG} 2>/dev/null"],
        capture_output=True, text=True,
    )
    server_lines = [l for l in result.stdout.splitlines() if l.strip()]
    server_last = _last_timestamp(server_lines)

    if server_last and server_last > local_last:
        print(f"Error: Server has a more recent commit — pull first.")
        print(f"  Local:  {local_last}")
        print(f"  Server: {server_last}")
        print(f"  Run: gnommo -p {project_path.name} down")
        return 1

    print(f"Pushing: {project_path.name}  (whole tree minus excludes, last commit: {local_last})")

    shared_root = _find_shared_assets_root(project_path)
    remote_shared = f"{server['path']}/shared_assets"

    # Pass 1: project files — whole tree, denylist excludes. --delete mirrors the local
    # (master) tree onto the server: files deleted locally are removed there too.
    # Excluded paths (processed outputs, out/, chunks, …) are protected from deletion.
    rsync_cmd = [
        "rsync", "-av", "--progress", "--delete",
        "-e", f"ssh -p {server['port']}",
        *[f"--exclude={p}" for p in _SYNC_EXCLUDES],
        f"{project_path}/",
        f"{server['user']}@{server['host']}:{remote_project}/",
    ]
    # Pass 2: shared assets — whole tree, same excludes.
    rsync_shared = [
        "rsync", "-av", "--progress",
        "-e", f"ssh -p {server['port']}",
        *[f"--exclude={p}" for p in _SYNC_EXCLUDES],
        f"{shared_root}/" if shared_root else "",
        f"{server['user']}@{server['host']}:{remote_shared}/",
    ]

    if dry_run:
        dry_project = rsync_cmd[:1] + ["--dry-run"] + rsync_cmd[1:]
        print("\n[DRY RUN] Would push project:")
        print(f"  {' '.join(dry_project)}")
        if shared_root:
            dry_shared = rsync_shared[:1] + ["--dry-run"] + rsync_shared[1:]
            print("\n[DRY RUN] Would push shared assets:")
            print(f"  {' '.join(dry_shared)}")
        return 0

    if verbose:
        print(f"  {' '.join(rsync_cmd)}")

    # Pass 1: project files
    subprocess.run([*ssh_cmd, f"mkdir -p {remote_project}"], check=True)
    result = subprocess.run(rsync_cmd)
    if result.returncode != 0:
        print(f"Error: rsync failed for project files (code {result.returncode})")
        return 1

    # Pass 2: shared assets
    if shared_root:
        print("\nPushing shared assets...")
        subprocess.run([*ssh_cmd, f"mkdir -p {remote_shared}"], check=True)
        result = subprocess.run(rsync_shared)
        if result.returncode != 0:
            print(f"Error: rsync failed for shared assets (code {result.returncode})")
            return 1

    print("\nDone.")
    return 0


def cmd_down(project_path: Path, verbose: bool, dry_run: bool) -> int:
    from .cache import load_server_config

    server = load_server_config()
    if server is None:
        _print_server_error()
        return 1

    remote_project = f"{server['path']}/{project_path.name}"

    # Verify the project exists on the server
    ssh_cmd = ["ssh", "-p", server["port"], f"{server['user']}@{server['host']}"]
    result = subprocess.run(
        [*ssh_cmd, f"test -d {remote_project} && echo ok"],
        capture_output=True, text=True,
    )
    if result.stdout.strip() != "ok":
        print(f"Error: Project not found on server: {remote_project}")
        return 1

    remote_log_result = subprocess.run(
        [*ssh_cmd, f"cat {remote_project}/{_COMMITS_LOG} 2>/dev/null"],
        capture_output=True, text=True,
    )
    server_lines = [l for l in remote_log_result.stdout.splitlines() if l.strip()]
    server_last = _last_timestamp(server_lines)

    print(f"Pulling: {project_path.name}  (server last commit: {server_last or 'none'})")

    project_path.mkdir(parents=True, exist_ok=True)

    # --delete mirrors the server (which the master `up`'d) onto this local tree:
    # files gone from the server are removed here too. Excluded paths — above all the
    # rig-only *_processed.* outputs — are protected, so a `down` on the rig never
    # deletes its own preprocess artifacts.
    rsync_cmd = [
        "rsync", "-av", "--progress", "--delete",
        "-e", f"ssh -p {server['port']}",
        *[f"--exclude={p}" for p in _SYNC_EXCLUDES],
        f"{server['user']}@{server['host']}:{remote_project}/",
        f"{project_path}/",
    ]

    remote_shared = f"{server['path']}/shared_assets"
    local_shared = project_path.parent / "shared_assets"
    rsync_shared_cmd = [
        "rsync", "-av", "--progress",
        "-e", f"ssh -p {server['port']}",
        *[f"--exclude={p}" for p in _SYNC_EXCLUDES],
        f"{server['user']}@{server['host']}:{remote_shared}/",
        f"{local_shared}/",
    ]

    if dry_run:
        rsync_cmd.insert(1, "--dry-run")
        print("\n[DRY RUN] Would pull project:")
        print(f"  {' '.join(rsync_cmd)}")
        print("\n[DRY RUN] Would pull shared assets:")
        print(f"  {' '.join(rsync_shared_cmd)}")
        return 0

    if verbose:
        print(f"  {' '.join(rsync_cmd)}")

    # Pass 1: project files
    result = subprocess.run(rsync_cmd)
    if result.returncode != 0:
        print(f"Error: rsync failed for project files (code {result.returncode})")
        return 1

    # Pass 2: shared assets (only if server has any)
    check = subprocess.run(
        [*ssh_cmd, f"test -d {remote_shared} && echo ok"],
        capture_output=True, text=True,
    )
    if check.stdout.strip() == "ok":
        print("\nPulling shared assets...")
        local_shared.mkdir(parents=True, exist_ok=True)
        result = subprocess.run(rsync_shared_cmd)
        if result.returncode != 0:
            print(f"Error: rsync failed for shared assets (code {result.returncode})")
            return 1

    print("\nDone.")
    return 0


def _print_server_error():
    print("Error: Server not configured. Add to ~/.gnommo.conf:")
    print("  [server]")
    print("  host = 192.168.1.100")
    print("  user = username")
    print("  path = /gnommo/project")
    print("  port = 22")
