#!/usr/bin/env python3
"""ASUCB Push Remediated File — replace a live Canvas course file in place.

Phase 4 Canvas delivery for file-remediation (plan §4, AC13). Mirrors
push_to_canvas.py discipline: backup -> upload (overwrite) -> verify ->
clear staging. Uploads the staged remediated file with on_duplicate=overwrite
into the SAME Canvas folder under the SAME display_name, so Canvas replaces
the file in place. See skills/file-remediation/SKILL.md Stage 4.
"""

import argparse
import hashlib
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path

import requests

sys.path.insert(0, os.path.dirname(__file__))
import canvas_api
from canvas_api import get_plugin_root

try:
    import backlog_manager
except Exception:
    backlog_manager = None

CONFIRM_VERBS = ("push", "approved", "deploy")


def _staging_files_dir(course_id):
    return (Path(get_plugin_root()) / "courses" / str(course_id)
            / "staging" / "files").resolve()


def _validate_staged_local(course_id, local_path):
    """Require --local to resolve inside courses/{id}/staging/files/.

    Write Safety Guard step 1 (stage first): this never pushes an arbitrary
    filesystem path — the remediated output must already be staged.
    """
    staging_dir = _staging_files_dir(course_id)
    try:
        resolved = Path(local_path).resolve()
    except OSError as exc:
        return None, f"Could not resolve --local path: {exc}"
    if not resolved.is_file():
        return None, f"Local file not found: {local_path}"
    if not resolved.is_relative_to(staging_dir):
        return None, (
            f"--local must be a staged file under {staging_dir} "
            "(copy the remediated output there first; see "
            "skills/file-remediation/SKILL.md Stage 4).")
    return resolved, None


def _backup_live_file(course_id, file_id, content_bytes, display_name, live_meta):
    """Timestamped binary backup of the live file.

    backup_manager.save_backup() is text/JSON-only (no binary entry in its
    EXTENSIONS map), so this mirrors its {timestamp}_{id}{ext} + checksum
    sidecar convention under courses/{id}/backups/files/ instead of forcing
    binary bytes through a text-only API.
    """
    ext = Path(display_name).suffix
    ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    backup_dir = Path(get_plugin_root()) / "courses" / str(course_id) / "backups" / "files"
    backup_dir.mkdir(parents=True, exist_ok=True)
    backup_path = backup_dir / f"{ts}_{file_id}{ext}"
    backup_path.write_bytes(content_bytes)
    (backup_dir / f"{ts}_{file_id}{ext}.meta.json").write_text(json.dumps({
        "course_id": str(course_id), "file_id": str(file_id),
        "display_name": display_name, "timestamp": ts,
        "checksum": hashlib.sha256(content_bytes).hexdigest(),
        "size_bytes": len(content_bytes),
        "canvas_updated_at": live_meta.get("updated_at"),
    }, indent=2), encoding="utf-8")
    return str(backup_path)


def push_remediated_file(course_id, file_id, local_path, confirm_verb):
    """Backup -> upload (overwrite) -> verify -> clear staging -> backlog close."""
    if confirm_verb not in CONFIRM_VERBS:
        return {"ok": False, "error":
                 f"--confirm-verb must be one of {list(CONFIRM_VERBS)}, got {confirm_verb!r}."}

    resolved_local, err = _validate_staged_local(course_id, local_path)
    if err:
        return {"ok": False, "error": err}

    config = canvas_api.get_config(course_id=course_id)
    canvas_api.require_course_id(config)

    live = canvas_api.get_file(config, file_id)
    if not live:
        return {"ok": False, "error": f"Canvas file {file_id} not found in course {course_id}."}

    display_name = live.get("display_name") or live.get("filename")
    folder_id = live.get("folder_id")
    content_type = live.get("content-type") or "application/octet-stream"
    download_url = live.get("url")
    if not display_name or not folder_id or not download_url:
        return {"ok": False, "error":
                 "Live file metadata missing display_name/folder_id/url; refusing to push."}

    try:
        resp = requests.get(download_url, headers=config["headers"], timeout=60)
        resp.raise_for_status()
        live_bytes = resp.content
    except requests.RequestException as exc:
        return {"ok": False, "error": f"Failed to download live file for backup: {exc}"}

    backup_path = _backup_live_file(course_id, file_id, live_bytes, display_name, live)

    new_file_id = canvas_api.upload_file(
        config, str(resolved_local), folder_id,
        content_type=content_type, filename=display_name,
    )
    if not new_file_id:
        return {"ok": False, "error": "Canvas upload failed.",
                 "backup_path": backup_path, "staging_preserved": True}

    # Post-write verify, before clearing staging. Canvas is expected to keep
    # the file id stable on a same-name/same-folder overwrite; if the
    # original id no longer reflects the pushed content, fall back to the id
    # upload_file returned rather than hard-failing on an id-stability
    # assumption this script can't verify live right now (expired token —
    # see tests/integration/test_live_file_replace.py).
    local_size = resolved_local.stat().st_size
    verify_id = file_id
    verified_meta = canvas_api.get_file(config, file_id)
    if not verified_meta or verified_meta.get("size") != local_size:
        verify_id = new_file_id
        verified_meta = canvas_api.get_file(config, new_file_id)

    size_ok = bool(verified_meta) and verified_meta.get("size") == local_size
    updated_advanced = (bool(verified_meta)
                         and verified_meta.get("updated_at") != live.get("updated_at"))
    if not (size_ok and updated_advanced):
        return {
            "ok": False,
            "error": ("Post-write verification failed: re-fetched Canvas file metadata "
                      f"does not show size={local_size} bytes with an advanced updated_at."),
            "backup_path": backup_path,
            "staging_preserved": True,
            "verified_file_id": str(verify_id) if verify_id else None,
        }

    resolved_findings = []
    if backlog_manager is not None:
        try:
            resolved_findings = backlog_manager.resolve_file_a11y(
                course_id, file_id=file_id, file_name=display_name)
        except Exception:
            resolved_findings = []

    try:
        resolved_local.unlink()
        sidecar = resolved_local.with_name(resolved_local.name + ".meta.json")
        if sidecar.exists():
            sidecar.unlink()
    except OSError:
        pass  # Clearing staging is best-effort; the push already verified.

    return {
        "ok": True,
        "course_id": str(course_id),
        "original_file_id": str(file_id),
        "verified_file_id": str(verify_id),
        "file_id_changed": str(verify_id) != str(file_id),
        "display_name": display_name,
        "backup_path": backup_path,
        "size_bytes": local_size,
        "canvas_url": f"https://{config['domain']}/courses/{course_id}/files/{verify_id}",
        "backlog_closed": resolved_findings,
    }


def main():
    parser = argparse.ArgumentParser(
        description="Replace a live Canvas course file with its remediated version "
                     "(backup -> upload overwrite -> verify -> clear staging).")
    parser.add_argument("--course-id", required=True)
    parser.add_argument("--file-id", required=True, help="Canvas file ID to replace")
    parser.add_argument("--local", required=True,
                        help="Path to the remediated file; must be staged under "
                             "courses/{course-id}/staging/files/")
    parser.add_argument("--confirm-verb", choices=list(CONFIRM_VERBS), required=True,
                        help="Explicit push verb (CLAUDE.md Write Safety Guard step 4). "
                             "Ambiguous affirmatives like 'ok'/'yes'/'confirm' are rejected.")
    parser.add_argument("--json", action="store_true", help="Emit JSON only")
    args = parser.parse_args()

    result = push_remediated_file(args.course_id, args.file_id, args.local, args.confirm_verb)

    if args.json:
        print(json.dumps(result, indent=2))
    elif result.get("ok"):
        print(f"Pushed. Canvas file: {result['canvas_url']}")
        print(f"Backup: {result['backup_path']}")
        if result.get("file_id_changed"):
            print(f"NOTE: file id changed on overwrite ({result['original_file_id']} "
                  f"-> {result['verified_file_id']}).")
        if result.get("backlog_closed"):
            print(f"Closed backlog findings: {', '.join(result['backlog_closed'])}")
    else:
        print(f"ERROR: {result.get('error')}")
        if result.get("backup_path"):
            print(f"Backup preserved at: {result['backup_path']}")

    if not result.get("ok"):
        sys.exit(1)


if __name__ == "__main__":
    main()
