package api

import (
	"archive/zip"
	"crypto/sha256"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"

	"archivmail/internal/audit"
	"archivmail/internal/index"
	"archivmail/internal/userstore"
	"archivmail/pkg/mailparser"
)

// handleExportEDiscovery runs a search server-side and streams all matching
// mails as a ZIP archive with a metadata CSV and README.
//
// POST /api/export/ediscovery
// Body: {case_name, q, from, to, date_from, date_to, has_attachment}
func (s *Server) handleExportEDiscovery(w http.ResponseWriter, r *http.Request) {
	var req struct {
		CaseName      string `json:"case_name"`
		Query         string `json:"q"`
		From          string `json:"from"`
		To            string `json:"to"`
		DateFrom      string `json:"date_from"`
		DateTo        string `json:"date_to"`
		HasAttachment *bool  `json:"has_attachment"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "invalid request body")
		return
	}

	caseName := strings.TrimSpace(req.CaseName)
	if caseName == "" {
		caseName = "archivmail-export"
	}

	sess := sessionFromCtx(r.Context())
	tenantID := tenantFromCtx(r.Context())

	// domain_auditor without tenant → deny
	if sess.Role == userstore.RoleDomainAuditor && tenantID == nil {
		writeError(w, http.StatusForbidden, "access denied")
		return
	}

	// Build search request — fetch up to 10 000 mails in one pass.
	searchReq := index.SearchRequest{
		Query:         req.Query,
		From:          req.From,
		To:            req.To,
		HasAttachment: req.HasAttachment,
		PageSize:      10000,
		Page:          1,
	}
	if req.DateFrom != "" {
		if t, err := time.Parse(time.DateOnly, req.DateFrom); err == nil {
			searchReq.DateFrom = &t
		}
	}
	if req.DateTo != "" {
		if t, err := time.Parse(time.DateOnly, req.DateTo); err == nil {
			t = t.Add(24*time.Hour - time.Second)
			searchReq.DateTo = &t
		}
	}

	// Choose index (per-tenant or global). PROJ-55: a tenant-scoped auditor
	// (tenant_id set) uses the per-tenant index like domain_auditor; only a
	// global auditor (no tenant_id) stays on the global index.
	searchIdx := s.idx
	usedTenantIndex := false
	if s.idxMgr != nil && tenantID != nil && !auditorIsGlobal(sess) {
		searchIdx = s.idxMgr.ForTenant(tenantID)
		usedTenantIndex = true
	}

	result, err := searchIdx.Search(searchReq)
	if err != nil {
		writeError(w, http.StatusInternalServerError, "search failed")
		return
	}

	// BUG-1 (PROJ-55): fail-closed tenant post-filter when we fell back to the
	// global index but the session is tenant-scoped (idxMgr not wired). Without
	// this a tenant-scoped auditor/domain_auditor would otherwise receive mails
	// of ALL tenants via eDiscovery. Mirrors the GetAllIDsByTenant fallback in
	// handleSearch (search_handlers.go).
	if tenantID != nil && !usedTenantIndex && len(result.Hits) > 0 && !auditorIsGlobal(sess) {
		allowedIDs, idErr := s.store.GetAllIDsByTenant(r.Context(), tenantID)
		if idErr != nil {
			writeError(w, http.StatusInternalServerError, "access check failed")
			return
		}
		allowed := make(map[string]struct{}, len(allowedIDs))
		for _, id := range allowedIDs {
			allowed[id] = struct{}{}
		}
		filtered := result.Hits[:0]
		for _, h := range result.Hits {
			if _, ok := allowed[h.ID]; ok {
				filtered = append(filtered, h)
			}
		}
		result.Hits = filtered
		result.Total = len(filtered)
	}

	// Global auditor (no tenant_id): restrict to no-tenant mails. A tenant-scoped
	// auditor is already constrained by the per-tenant index above (PROJ-55).
	var auditorAllowed map[string]struct{}
	if auditorIsGlobal(sess) {
		ids, err := s.store.GetAllIDsWithoutTenant(r.Context())
		if err != nil {
			writeError(w, http.StatusInternalServerError, "access check failed")
			return
		}
		auditorAllowed = make(map[string]struct{}, len(ids))
		for _, id := range ids {
			auditorAllowed[id] = struct{}{}
		}
	}

	// User: only own mails
	var userEmail string
	if sess.Role == userstore.RoleUser {
		u, err := s.users.GetByUsername(sess.Username)
		if err != nil {
			writeError(w, http.StatusInternalServerError, "user lookup failed")
			return
		}
		userEmail = strings.ToLower(u.Email)
		if userEmail == "" {
			writeError(w, http.StatusForbidden, "access denied")
			return
		}
	}

	// Stream ZIP response
	safeCase := sanitizeExportFilename(caseName)
	zipName := fmt.Sprintf("%s-%s.zip", safeCase, time.Now().UTC().Format("20060102-150405"))
	w.Header().Set("Content-Type", "application/zip")
	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, zipName))

	zw := zip.NewWriter(w)
	defer zw.Close()

	type metaRow struct {
		id            string
		messageID     string
		from          string
		to            string
		cc            string
		subject       string
		date          string
		sizeBytes     int
		hasAttach     bool
		sha256Hash    string
		threadID      string
		emlFilename   string
	}
	var rows []metaRow
	exported := 0

	for _, hit := range result.Hits {
		id := hit.ID

		if auditorAllowed != nil {
			if _, ok := auditorAllowed[id]; !ok {
				continue
			}
		}

		raw, err := s.store.Load(id)
		if err != nil {
			continue
		}

		pm, err := mailparser.Parse(raw)
		if err != nil {
			continue
		}

		if userEmail != "" && !mailBelongsToUser(pm, userEmail) {
			continue
		}

		// Compute SHA-256 of raw mail
		sum := sha256.Sum256(raw)
		hash := fmt.Sprintf("%x", sum[:])

		safeID := id
		if len(safeID) > 16 {
			safeID = safeID[:16]
		}
		emlFilename := fmt.Sprintf("%04d_%s.eml", exported+1, safeID)

		fw, err := zw.Create(emlFilename)
		if err != nil {
			continue
		}
		if _, err := fw.Write(raw); err != nil {
			continue
		}

		var dateStr string
		if !pm.Date.IsZero() {
			dateStr = pm.Date.UTC().Format(time.RFC3339)
		}

		// Thread ID from DB
		var threadID string
		if ti, err := s.store.GetThreadInfo(r.Context(), []string{id}); err == nil {
			if info, ok := ti[id]; ok {
				threadID = info.ThreadID
			}
		}

		rows = append(rows, metaRow{
			id:          id,
			messageID:   pm.MessageID,
			from:        pm.From,
			to:          strings.Join(pm.To, ", "),
			cc:          strings.Join(pm.CC, ", "),
			subject:     pm.Subject,
			date:        dateStr,
			sizeBytes:   len(raw),
			hasAttach:   len(pm.Attachments) > 0,
			sha256Hash:  hash,
			threadID:    threadID,
			emlFilename: emlFilename,
		})
		exported++
	}

	// Write metadata.csv
	if mf, err := zw.Create("metadata.csv"); err == nil {
		cw := csv.NewWriter(mf)
		_ = cw.Write([]string{
			"filename", "id", "message_id", "from", "to", "cc",
			"subject", "date", "size_bytes", "has_attachments", "sha256", "thread_id",
		})
		for _, row := range rows {
			hasAttach := "false"
			if row.hasAttach {
				hasAttach = "true"
			}
			_ = cw.Write([]string{
				row.emlFilename,
				row.id,
				row.messageID,
				row.from,
				row.to,
				row.cc,
				row.subject,
				row.date,
				fmt.Sprintf("%d", row.sizeBytes),
				hasAttach,
				row.sha256Hash,
				row.threadID,
			})
		}
		cw.Flush()
	}

	// Write README.txt
	if rf, err := zw.Create("README.txt"); err == nil {
		var sb strings.Builder
		sb.WriteString("archivmail eDiscovery Export\n")
		sb.WriteString(strings.Repeat("=", 40) + "\n\n")
		sb.WriteString(fmt.Sprintf("Case:        %s\n", caseName))
		sb.WriteString(fmt.Sprintf("Exported at: %s\n", time.Now().UTC().Format(time.RFC3339)))
		sb.WriteString(fmt.Sprintf("Exported by: %s (%s)\n", sess.Username, sess.Role))
		sb.WriteString(fmt.Sprintf("Total mails: %d\n\n", exported))
		sb.WriteString("Filter parameters:\n")
		if req.Query != "" {
			sb.WriteString(fmt.Sprintf("  Query:    %s\n", req.Query))
		}
		if req.From != "" {
			sb.WriteString(fmt.Sprintf("  From:     %s\n", req.From))
		}
		if req.To != "" {
			sb.WriteString(fmt.Sprintf("  To:       %s\n", req.To))
		}
		if req.DateFrom != "" {
			sb.WriteString(fmt.Sprintf("  From date: %s\n", req.DateFrom))
		}
		if req.DateTo != "" {
			sb.WriteString(fmt.Sprintf("  To date:   %s\n", req.DateTo))
		}
		if req.Query == "" && req.From == "" && req.To == "" && req.DateFrom == "" && req.DateTo == "" {
			sb.WriteString("  (no filters — all archived mails)\n")
		}
		sb.WriteString("\nFiles:\n")
		sb.WriteString("  metadata.csv  — full metadata for all exported mails\n")
		sb.WriteString("  *.eml         — individual mail files (RFC 2822)\n")
		_, _ = rf.Write([]byte(sb.String()))
	}

	s.audlog.Log(audit.Entry{
		EventType: audit.EventExport,
		Username:  sess.Username,
		TenantID:  sess.TenantID,
		IPAddress: s.remoteIP(r),
		Detail:    fmt.Sprintf("ediscovery: case=%q mails=%d", caseName, exported),
		Success:   true,
	})
}

// sanitizeExportFilename replaces characters unsafe for filenames with underscores.
func sanitizeExportFilename(s string) string {
	var b strings.Builder
	for _, r := range s {
		switch {
		case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9',
			r == '-', r == '_', r == '.':
			b.WriteRune(r)
		default:
			b.WriteRune('_')
		}
	}
	result := b.String()
	if len(result) > 64 {
		result = result[:64]
	}
	if result == "" {
		result = "export"
	}
	return result
}
