package handlers

// Admin Backup Mode handlers (t-paliad-246 / m/paliad#77 Slice A).
//
//   POST /api/admin/backups/run           — kick off an on-demand backup
//   GET  /api/admin/backups               — chronological list
//   GET  /api/admin/backups/{id}          — single catalog row
//   GET  /api/admin/backups/{id}/file     — stream the artifact (records
//                                            a backup_downloaded audit row)
//   GET  /admin/backups                   — admin page (SPA shell)
//
// Authorisation: every route registers behind adminGate(users, …) in
// handlers.go, so every handler in this file can assume the caller is a
// global_admin and only validate the request shape.
//
// The runner is wired in cmd/server/main.go only when PALIAD_EXPORT_DIR
// is set. When unset, every handler returns 503 — same shape as
// requireDB.

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/google/uuid"

	"mgit.msbls.de/m/paliad/internal/services"
)

// backupRequestTimeout caps a single on-demand backup. At firm-scale
// data shapes (today: ~600 user-content rows + ~1000 reference rows)
// a backup runs sub-second; the watchdog surfaces "stuck" as a 500
// instead of letting the client hang forever.
const backupRequestTimeout = 5 * time.Minute

// requireBackup writes a 503 if the BackupRunner is not wired (typically
// PALIAD_EXPORT_DIR is unset) and returns false. Mirrors requireDB.
func requireBackup(w http.ResponseWriter) bool {
	if dbSvc == nil || dbSvc.backup == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{
			"error": "backup service not configured — set PALIAD_EXPORT_DIR on the server",
		})
		return false
	}
	return true
}

// handleAdminBackupsPage renders the /admin/backups SPA shell. The
// catalog rows are fetched client-side via /api/admin/backups.
func handleAdminBackupsPage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "dist/admin-backups.html")
}

// handleAdminRunBackup kicks off a synchronous on-demand backup and
// returns the resulting BackupSummary as JSON. Synchronous: at firm-
// scale the whole run is under 5s; an async path with polling is Slice
// B (the scheduler reuses the same runner internally).
//
// Returns 201 on success with the catalog row, 500 on failure (the
// catalog/audit rows are still flipped to failed/backup_failed before
// the response).
func handleAdminRunBackup(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) || !requireBackup(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}

	ctx, cancel := context.WithTimeout(r.Context(), backupRequestTimeout)
	defer cancel()

	user, err := dbSvc.users.GetByID(ctx, uid)
	if err != nil || user == nil {
		log.Printf("backup: user lookup failed for %s: %v", uid, err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "user lookup failed",
		})
		return
	}

	actor := services.BackupActor{
		ID:    &uid,
		Email: user.Email,
		Label: user.DisplayName,
	}
	result, err := dbSvc.backup.Run(ctx, services.BackupKindOnDemand, actor)
	if err != nil {
		log.Printf("backup: Run failed for admin=%s: %v", uid, err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "backup generation failed: " + err.Error(),
		})
		return
	}

	// Return the freshly-written catalog row so the UI doesn't need a
	// follow-up GET to render the new line item.
	row, err := dbSvc.backup.GetBackup(ctx, result.ID)
	if err != nil {
		// The backup did succeed — log + return the bare result.
		log.Printf("backup: post-run GetBackup failed for %s: %v", result.ID, err)
		writeJSON(w, http.StatusCreated, result)
		return
	}
	writeJSON(w, http.StatusCreated, row)
}

// handleAdminListBackups returns the most recent N catalog rows as
// JSON. ?limit=N caps the page (default 100).
func handleAdminListBackups(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) || !requireBackup(w) {
		return
	}
	limit := 100
	if q := strings.TrimSpace(r.URL.Query().Get("limit")); q != "" {
		if n, err := strconv.Atoi(q); err == nil && n > 0 && n <= 500 {
			limit = n
		}
	}
	rows, err := dbSvc.backup.ListBackups(r.Context(), limit)
	if err != nil {
		log.Printf("backup: list failed: %v", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "list failed",
		})
		return
	}
	if rows == nil {
		rows = []services.BackupSummary{}
	}
	writeJSON(w, http.StatusOK, rows)
}

// handleAdminGetBackup returns one catalog row. Used by the UI for
// "is the backup I just kicked off done yet?" polling — though at the
// synchronous shape today this rarely matters.
func handleAdminGetBackup(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) || !requireBackup(w) {
		return
	}
	id, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
		return
	}
	row, err := dbSvc.backup.GetBackup(r.Context(), id)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
			return
		}
		log.Printf("backup: get failed for %s: %v", id, err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "get failed"})
		return
	}
	writeJSON(w, http.StatusOK, row)
}

// handleAdminDownloadBackup streams the artifact bytes through the
// ArtifactStore (LocalDiskStore for v1). Records a backup_downloaded
// audit row before flushing.
//
// 404 if the catalog row is missing; 410 (Gone) if the artifact was
// already lifecycle-deleted; 409 if status is not 'done'; 500 on any
// store/IO error.
func handleAdminDownloadBackup(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) || !requireBackup(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	id, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
		return
	}

	row, err := dbSvc.backup.GetBackup(r.Context(), id)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
			return
		}
		log.Printf("backup: download GetBackup failed for %s: %v", id, err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "get failed"})
		return
	}
	if row.Status != services.BackupStatusDone || row.StorageURI == nil {
		writeJSON(w, http.StatusConflict, map[string]string{
			"error":  "backup not available for download",
			"status": row.Status,
		})
		return
	}
	if row.DeletedAt != nil {
		// 410 Gone — the artifact is past its retention window. Catalog
		// row stays as the audit trail; clients should not retry.
		writeJSON(w, http.StatusGone, map[string]string{
			"error": "artifact has been removed (retention)",
		})
		return
	}

	rc, size, err := dbSvc.backup.Store().Get(r.Context(), *row.StorageURI)
	if err != nil {
		log.Printf("backup: download store.Get failed for %s: %v", id, err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "store read failed"})
		return
	}
	defer rc.Close()

	// Record the download audit row before flushing. If the audit
	// write fails we still serve the file (the user can see it; the
	// chain just missed a row — surface in logs).
	user, uErr := dbSvc.users.GetByID(r.Context(), uid)
	if uErr == nil && user != nil {
		auditErr := dbSvc.backup.RecordDownload(r.Context(), id, services.BackupActor{
			ID:    &uid,
			Email: user.Email,
			Label: user.DisplayName,
		})
		if auditErr != nil {
			log.Printf("backup: RecordDownload failed for %s by %s: %v", id, uid, auditErr)
		}
	} else if uErr != nil {
		log.Printf("backup: user lookup for audit failed (%s): %v", uid, uErr)
	}

	filename := fmt.Sprintf("paliad-backup-%s.zip", row.StartedAt.UTC().Format("20060102T1504Z"))
	w.Header().Set("Content-Type", "application/zip")
	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename=%q`, filename))
	w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
	w.Header().Set("X-Paliad-Backup-Id", id.String())
	if _, err := io.Copy(w, rc); err != nil {
		log.Printf("backup: response write failed for %s: %v", id, err)
	}
}
