package handlers

// Data-export handlers (t-paliad-214).
//
// Slice 1: personal scope
//   GET /api/me/export → streams a personal-scope export .zip
//
// Slice 2: project subtree scope
//   GET /api/projects/{id}/export?direct_only=0|1 → streams a project-subtree
//   export .zip
//
// Slice 3 (org, async) lands in a follow-up.
//
// Authentication: the existing protected mux middleware (auth.Middleware +
// auth.WithUserID) populates the user UUID in the context. Slice 1 gates
// only on authentication; Slice 2 adds a §4 responsibility + global_admin
// check via handleProjectExportGate.

import (
	"bytes"
	"context"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"time"

	"github.com/google/uuid"

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

// exportRequestTimeout caps any single export request. Personal-scope
// exports at firm-scale data shape complete in well under this; the
// timeout is the watchdog that surfaces "too large for sync" loudly
// (the user gets a 503 and slice 3's async path becomes the answer).
const exportRequestTimeout = 30 * time.Second

// handleMeExport streams the caller's personal-scope export .zip.
//
// Order of operations:
//
//	1. Validate auth + db wiring.
//	2. Look up the caller's user row for actor_email / actor_label.
//	3. Write an audit row (event_type='data_export', scope='personal').
//	4. Run the export into an in-memory buffer (so we can patch the
//	   audit row with file_size_bytes before flushing to the client).
//	5. Set headers + flush.
//	6. Patch the audit row with success (row_counts + file_size).
//	   On any error after step 3, the audit row is patched as failed.
func handleMeExport(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if dbSvc.export == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{
			"error": "export service not configured",
		})
		return
	}

	// Apply the per-request watchdog.
	ctx, cancel := context.WithTimeout(r.Context(), exportRequestTimeout)
	defer cancel()

	user, err := dbSvc.users.GetByID(ctx, uid)
	if err != nil || user == nil {
		log.Printf("export: user lookup failed for %s: %v", uid, err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "user lookup failed",
		})
		return
	}
	spec := services.ExportSpec{
		Scope:       services.ExportScopePersonal,
		ActorID:     uid,
		ActorEmail:  user.Email,
		ActorLabel:  user.DisplayName,
		GeneratedAt: time.Now().UTC(),
	}

	auditID, err := dbSvc.export.WriteAuditRow(ctx, spec)
	if err != nil {
		log.Printf("export: audit insert failed for %s: %v", uid, err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "audit write failed",
		})
		return
	}

	// Generate into a memory buffer so we can size + audit-patch BEFORE
	// writing to the response (otherwise headers are committed and we
	// can't return a 500 if anything fails). At personal scale this is a
	// sub-megabyte buffer.
	var buf bytes.Buffer
	meta, err := dbSvc.export.WritePersonal(ctx, &buf, spec)
	if err != nil {
		dbSvc.export.PatchAuditRowFailure(context.Background(), auditID, err.Error())
		log.Printf("export: WritePersonal failed for %s (audit=%s): %v", uid, auditID, err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "export generation failed",
		})
		return
	}

	filename := services.ExportFilename(services.ExportScopePersonal, "", uuid.Nil, spec.GeneratedAt)
	size := int64(buf.Len())

	if err := dbSvc.export.PatchAuditRowSuccess(ctx, auditID, meta, size); err != nil {
		// Audit-patch failure isn't fatal to the user — they still get
		// their export. Log it; the data already left the system.
		log.Printf("export: audit patch failed for %s (audit=%s): %v", uid, auditID, err)
	}

	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-Export-Audit-Id", auditID.String())
	if _, err := w.Write(buf.Bytes()); err != nil {
		// Connection dropped mid-flush — the user didn't get the file.
		// We don't patch the audit row a second time; the success patch
		// already recorded the row counts. A separate event would be
		// noise (the failure is at the network layer, not in our path).
		log.Printf("export: response write failed for %s (audit=%s): %v", uid, auditID, err)
	}
}

// handleProjectExport streams the project-subtree export .zip for the
// project named in the URL path.
//
// Authorization (Slice 2 §4):
//
//   - caller must be authenticated (handled by the mux middleware),
//   - caller must pass paliad.can_see_project(rootID) — enforced via
//     ProjectService.GetByID returning ErrNotVisible → 404,
//   - caller must be on paliad.project_teams for the root with
//     responsibility ∈ {lead, member}, OR be a global_admin.
//     Observers + Externals see but cannot extract — 403 bilingual.
//
// Query params:
//   - ?direct_only=1 narrows the export to the root project only (no
//     descendants). Default = subtree-inclusive.
func handleProjectExport(w http.ResponseWriter, r *http.Request) {
	if !requireDB(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	if dbSvc.export == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{
			"error": "export service not configured",
		})
		return
	}
	rootID, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": "invalid project id",
		})
		return
	}

	directOnly := false
	if q := r.URL.Query().Get("direct_only"); q == "1" || q == "true" {
		directOnly = true
	}

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

	// Visibility gate (a + b): GetByID returns ErrNotVisible when the
	// caller can't see the project, which we map to 404. The handler
	// stays oblivious to whether the project doesn't exist or simply
	// isn't visible — that's by design (RLS-style opacity).
	project, err := dbSvc.projects.GetByID(ctx, uid, rootID)
	if err != nil {
		writeServiceError(w, err)
		return
	}

	// Authority gate (c): direct-team responsibility ∈ {lead, member} OR
	// global_admin. Derived-only-via-partner-unit users (DerivedPeer)
	// don't qualify for extraction — m's Q1 lock-in.
	allowed, err := callerCanExportProject(ctx, uid, rootID)
	if err != nil {
		log.Printf("export: authority check failed for user=%s project=%s: %v", uid, rootID, err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "authority check failed",
		})
		return
	}
	if !allowed {
		// Bilingual 403 per Q7. Pattern matches mapApprovalError style.
		writeJSON(w, http.StatusForbidden, map[string]string{
			"code":    "export_not_authorized",
			"message": "Datenexport ist nur Team-Mitgliedern (Lead / Member) vorbehalten. / Data export is restricted to project team members (lead / member).",
		})
		return
	}

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

	spec := services.ExportSpec{
		Scope:          services.ExportScopeProject,
		ScopeRoot:      &rootID,
		ScopeRootLabel: project.Title,
		ScopeRootPath:  project.Path,
		DirectOnly:     directOnly,
		ActorID:        uid,
		ActorEmail:     user.Email,
		ActorLabel:     user.DisplayName,
		GeneratedAt:    time.Now().UTC(),
	}

	auditID, err := dbSvc.export.WriteAuditRow(ctx, spec)
	if err != nil {
		log.Printf("export: audit insert failed for %s/project=%s: %v", uid, rootID, err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "audit write failed",
		})
		return
	}

	var buf bytes.Buffer
	meta, err := dbSvc.export.WriteProject(ctx, &buf, spec)
	if err != nil {
		dbSvc.export.PatchAuditRowFailure(context.Background(), auditID, err.Error())
		log.Printf("export: WriteProject failed for %s/project=%s (audit=%s): %v", uid, rootID, auditID, err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{
			"error": "export generation failed",
		})
		return
	}

	filename := services.ExportFilename(services.ExportScopeProject, project.Title, rootID, spec.GeneratedAt)
	size := int64(buf.Len())

	if err := dbSvc.export.PatchAuditRowSuccess(ctx, auditID, meta, size); err != nil {
		log.Printf("export: audit patch failed for %s/project=%s (audit=%s): %v", uid, rootID, auditID, err)
	}

	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-Export-Audit-Id", auditID.String())
	if _, err := w.Write(buf.Bytes()); err != nil {
		log.Printf("export: response write failed for %s/project=%s (audit=%s): %v", uid, rootID, auditID, err)
	}
}

// callerCanExportProject is the §4 authority check:
//
//   - global_admin can extract anything anywhere.
//   - else: caller must be on paliad.project_teams for the root with
//     responsibility ∈ {lead, member}.
//
// One query, parameterised; returns the boolean. Errors surface to the
// handler as 500.
func callerCanExportProject(ctx context.Context, userID, projectID uuid.UUID) (bool, error) {
	const q = `
SELECT
  EXISTS (
    SELECT 1 FROM paliad.users u
     WHERE u.id = $1 AND u.global_role = 'global_admin'
  ) OR EXISTS (
    SELECT 1 FROM paliad.project_teams pt
     WHERE pt.user_id = $1
       AND pt.project_id = $2
       AND pt.responsibility IN ('lead', 'member')
  )
`
	var ok bool
	if err := dbSvc.projects.DB().QueryRowContext(ctx, q, userID, projectID).Scan(&ok); err != nil {
		return false, err
	}
	return ok, nil
}
