#!/usr/bin/env python3
"""ASUCB Retire File — delete-with-backup for stray/orphaned Canvas files.

Field lesson (2026-07-22, STP530/HSC 210): "retire = unpublish" fails —
Anthology Ally scans ALL course files regardless of publish state, so an
unpublished stray still drags the course accessibility score. Correct
semantics: backup bytes locally, then DELETE from Canvas. Deletion is
irreversible on Canvas; the local backup + `--restore` is the safety net.
Mirrors push_remediated_file.py discipline: verb gate -> backup -> verify ->
mutate -> verify. See skills/file-remediation/SKILL.md.
"""

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

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


def _backup_dir(course_id):
    d = Path(get_plugin_root()) / "courses" / str(course_id) / "backups" / "retired-files"
    d.mkdir(parents=True, exist_ok=True)
    return d


def retire_file(course_id, file_id, confirm_verb):
    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}."}

    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")
    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 retire."}

    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}"}

    declared = live.get("size")
    if declared is not None and int(declared) != len(live_bytes):
        return {"ok": False, "error":
                 f"Downloaded {len(live_bytes)}B but Canvas declares {declared}B; "
                 "refusing to delete against a possibly-truncated backup."}

    ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    backup_path = _backup_dir(course_id) / f"{ts}_{file_id}-{display_name}"
    n = 1
    while backup_path.exists():  # never overwrite an existing safety copy
        backup_path = _backup_dir(course_id) / f"{ts}_{file_id}-{n}-{display_name}"
        n += 1
    backup_path.write_bytes(live_bytes)

    # Hard abort before delete unless the bytes READ BACK from disk match.
    checksum = hashlib.sha256(live_bytes).hexdigest()
    try:
        readback_ok = hashlib.sha256(backup_path.read_bytes()).hexdigest() == checksum
    except OSError:
        readback_ok = False
    if not readback_ok:
        return {"ok": False, "error": "Backup read-back verification failed before delete; aborting.",
                 "backup_path": str(backup_path)}

    meta_path = backup_path.with_suffix(backup_path.suffix + ".meta.json")
    meta = {
        "course_id": str(course_id),
        "file_id": str(file_id),
        "display_name": display_name,
        "folder_id": folder_id,
        "size_bytes": len(live_bytes),
        "checksum": checksum,
        "deleted_at": ts,
        "canvas_updated_at": live.get("updated_at"),
    }
    meta_path.write_text(json.dumps(meta, indent=2), encoding="utf-8")

    if not canvas_api.delete_file(config, file_id, confirm=True):
        return {"ok": False, "error": "Canvas delete_file call failed.",
                 "backup_path": str(backup_path), "meta_path": str(meta_path)}

    post = canvas_api.get_file(config, file_id)
    verified_deleted = post is None or str(post.get("workflow_state", "")) == "deleted"
    if not verified_deleted:
        return {"ok": False, "error":
                 "delete_file reported success but the file is still present on Canvas; "
                 "backup preserved.", "backup_path": str(backup_path),
                 "meta_path": str(meta_path), "verified_deleted": False}

    return {
        "ok": True,
        "course_id": str(course_id),
        "file_id": str(file_id),
        "display_name": display_name,
        "backup_path": str(backup_path),
        "meta_path": str(meta_path),
        "verified_deleted": verified_deleted,
    }


def restore_file(course_id, backup_path, folder_id, confirm_verb):
    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}."}

    backup = Path(backup_path)
    meta_path = backup.with_suffix(backup.suffix + ".meta.json")
    if not backup.is_file():
        return {"ok": False, "error": f"Backup file not found: {backup_path}"}
    if not meta_path.is_file():
        return {"ok": False, "error": f"Backup metadata sidecar not found: {meta_path}"}

    meta = json.loads(meta_path.read_text(encoding="utf-8"))
    display_name = meta["display_name"]
    target_folder = folder_id or meta.get("folder_id")
    if not target_folder:
        return {"ok": False, "error":
                 "No folder recorded in backup metadata and no --folder-id given; "
                 "pass --folder-id (the original folder may have been deleted)."}

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

    new_file_id = canvas_api.upload_file(
        config, str(backup), target_folder, filename=display_name,
    )
    if not new_file_id:
        return {"ok": False, "error": "Canvas upload failed during restore."}

    restored = canvas_api.get_file(config, new_file_id)
    return {
        "ok": bool(restored),
        "course_id": str(course_id),
        "original_file_id": meta["file_id"],
        "new_file_id": str(new_file_id),
        "note": "Canvas assigns a new file id on restore; the original id is not resurrected.",
        "display_name": display_name,
        "canvas_url": f"https://{config['domain']}/courses/{course_id}/files/{new_file_id}",
    }


def main():
    parser = argparse.ArgumentParser(
        description="Retire a stray Canvas file: backup bytes locally, then delete from Canvas. "
                     "Ally scans unpublished files too, so unpublish alone doesn't help.")
    parser.add_argument("--course-id", required=True)
    parser.add_argument("--file-id", help="Canvas file ID to retire (delete-with-backup).")
    parser.add_argument("--restore", metavar="BACKUP_PATH",
                        help="Restore a previously retired file from its backup path.")
    parser.add_argument("--folder-id", help="Restore only: target folder (defaults to the "
                        "folder recorded in the backup's .meta.json).")
    parser.add_argument("--confirm-verb", required=True,
                        help=f"Explicit verb, one of {list(CONFIRM_VERBS)}.")
    parser.add_argument("--json", action="store_true", help="Emit JSON only")
    args = parser.parse_args()

    if args.restore:
        result = restore_file(args.course_id, args.restore, args.folder_id, args.confirm_verb)
    elif args.file_id:
        result = retire_file(args.course_id, args.file_id, args.confirm_verb)
    else:
        parser.error("either --file-id (retire) or --restore (restore) is required")
        return

    if args.json:
        print(json.dumps(result, indent=2))
    elif result.get("ok"):
        if args.restore:
            print(f"Restored. Canvas file: {result['canvas_url']}")
            print(result["note"])
        else:
            print(f"Retired file {result['file_id']} ({result['display_name']}).")
            print(f"Backup: {result['backup_path']}")
            print(f"Verified deleted: {result['verified_deleted']}")
    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()
