package handlers

import (
	"encoding/csv"
	"encoding/json"
	"fmt"
	"net/http"
	"sort"
	"strings"
	"time"

	"github.com/breakpilot/ai-compliance-sdk/internal/iace"
	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
)

// projectMetadataRoot is the shape we store inside iace_projects.metadata.
// We only own the "clarification_answers" key; everything else is preserved
// as opaque JSON so we don't trample on existing fields (limits_form, etc).
type projectMetadataRoot map[string]json.RawMessage

const clarificationAnswersKey = "clarification_answers"

// readClarificationAnswers parses project.metadata and returns the
// clarification_answers map. Missing/empty metadata yields an empty map.
func readClarificationAnswers(meta json.RawMessage) (map[string]iace.ClarificationAnswer, projectMetadataRoot) {
	root := projectMetadataRoot{}
	if len(meta) > 0 {
		_ = json.Unmarshal(meta, &root)
	}
	answers := map[string]iace.ClarificationAnswer{}
	if raw, ok := root[clarificationAnswersKey]; ok && len(raw) > 0 {
		_ = json.Unmarshal(raw, &answers)
	}
	return answers, root
}

// reconstructHazardPatterns re-runs the pattern engine for the project's
// narrative so we can map each hazard back to the patterns that fired for
// it. The Hazard table itself doesn't persist the source-pattern list, so
// this is the only way to know "which clarifications apply to which hazard".
func (h *IACEHandler) reconstructHazardPatterns(narrative string, machineType string, hazards []iace.Hazard) map[uuid.UUID][]string {
	parsed := iace.ParseNarrative(narrative, machineType)
	compIDs := make([]string, 0, len(parsed.Components))
	for _, c := range parsed.Components {
		compIDs = append(compIDs, c.LibraryID)
	}
	energyIDs := make([]string, 0, len(parsed.EnergySources))
	for _, e := range parsed.EnergySources {
		energyIDs = append(energyIDs, e.SourceID)
	}
	engine := iace.NewPatternEngine()
	out := engine.Match(iace.MatchInput{
		ComponentLibraryIDs: compIDs,
		EnergySourceIDs:     energyIDs,
		LifecyclePhases:     parsed.LifecyclePhases,
		CustomTags:          parsed.CustomTags,
		OperationalStates:   parsed.OperationalStates,
		StateTransitions:    parsed.StateTransitions,
		HumanRoles:          parsed.Roles,
		MachineTypes:        []string{machineType},
	})

	// Map hazard.HazardousZone → set of HP-IDs by substring-matching the
	// pattern's ZoneDE. The hazard table doesn't keep a back-pointer to
	// the source pattern, so this approximation re-runs pattern matching
	// against the narrative and matches by normalised zone.
	hazardToPatterns := map[uuid.UUID][]string{}
	for _, hz := range hazards {
		hzZone := normalizeKey(hz.HazardousZone)
		if hzZone == "" {
			continue
		}
		for _, m := range out.MatchedPatterns {
			pz := normalizeKey(m.ZoneDE)
			if pz == "" {
				continue
			}
			if pz == hzZone || containsSubstring(hzZone, pz) || containsSubstring(pz, hzZone) {
				hazardToPatterns[hz.ID] = appendUnique(hazardToPatterns[hz.ID], m.PatternID)
			}
		}
	}
	return hazardToPatterns
}

func normalizeKey(s string) string {
	s = iace.NormalizeDEPublic(s)
	out := []rune{}
	for _, r := range s {
		switch r {
		case ',', '/', '(', ')', '-', '.', ':', ';':
			out = append(out, ' ')
		default:
			out = append(out, r)
		}
	}
	return string(out)
}

func appendUnique(slice []string, s string) []string {
	for _, x := range slice {
		if x == s {
			return slice
		}
	}
	return append(slice, s)
}

// ListClarifications handles GET /projects/:id/clarifications.
// Returns the aggregated clarification list with affected-hazard cross-refs
// and the persisted answer state.
//
// Phase 3 storage model: answers live in the iace_clarifications table
// when migration 028 has been applied. The JSONB fallback in
// project.metadata.clarification_answers is still read so projects that
// were answered before the migration keep their state until the one-shot
// upcopy runs.
func (h *IACEHandler) ListClarifications(c *gin.Context) {
	projectID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
		return
	}
	ctx := c.Request.Context()
	project, err := h.store.GetProject(ctx, projectID)
	if err != nil || project == nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
		return
	}
	hazards, _ := h.store.ListHazards(ctx, projectID)

	// Primary: relational answers
	answers := map[string]iace.ClarificationAnswer{}
	if rows, rerr := h.store.ListClarificationsForProject(ctx, projectID); rerr == nil {
		for _, r := range rows {
			answeredAt := ""
			if r.AnsweredAt != nil {
				answeredAt = r.AnsweredAt.UTC().Format(time.RFC3339)
			}
			answers[r.ClarificationKey] = iace.ClarificationAnswer{
				Status:     r.Status,
				Answer:     r.Answer,
				Reasoning:  r.Reasoning,
				AnsweredBy: r.AnsweredBy,
				AnsweredAt: answeredAt,
				AssignedTo: r.AssignedTo,
			}
		}
	}
	// Fallback: JSONB legacy answers (keep until one-shot upcopy is done)
	if legacy, _ := readClarificationAnswers(project.Metadata); len(legacy) > 0 {
		for k, v := range legacy {
			if _, ok := answers[k]; !ok {
				answers[k] = v
			}
		}
	}
	narrative := extractNarrativeFromMetadata(project.Metadata)
	hazardToPatterns := h.reconstructHazardPatterns(narrative, project.MachineType, hazards)
	manufHits := iace.LookupManufacturerFeaturesInText(narrative)

	clarifications := iace.BuildProjectClarifications(hazards, hazardToPatterns, manufHits, answers)
	sort.Slice(clarifications, func(i, j int) bool {
		// Open first, then answered. Within a status, group by category, then by source.
		if clarifications[i].Status != clarifications[j].Status {
			return clarifications[i].Status == "open"
		}
		if clarifications[i].Category != clarifications[j].Category {
			return clarifications[i].Category < clarifications[j].Category
		}
		return clarifications[i].Source < clarifications[j].Source
	})

	openCount, answeredCount := 0, 0
	for _, cl := range clarifications {
		switch cl.Status {
		case "answered", "not_relevant":
			answeredCount++
		default:
			openCount++
		}
	}
	c.JSON(http.StatusOK, gin.H{
		"clarifications": clarifications,
		"open_count":     openCount,
		"answered_count": answeredCount,
		"total":          len(clarifications),
	})
}

// AnswerClarificationRequest is the request body for POST .../answer.
type AnswerClarificationRequest struct {
	Status     string `json:"status"` // open | in_progress | answered | not_relevant
	Answer     string `json:"answer"` // ja | nein | teilweise
	Reasoning  string `json:"reasoning"`
	AnsweredBy string `json:"answered_by"`
	AssignedTo string `json:"assigned_to"`
	// Snapshot fields written into the new table on first contact so the
	// audit trail does not break if the pattern library changes later.
	Question       string   `json:"question,omitempty"`
	Source         string   `json:"source,omitempty"`
	Category       string   `json:"category,omitempty"`
	NormReferences []string `json:"norm_references,omitempty"`
}

// AnswerClarification handles POST /projects/:id/clarifications/:cid/answer.
// Upserts the answer in iace_clarifications (Phase 3). Old JSONB answers
// remain readable but are no longer written.
func (h *IACEHandler) AnswerClarification(c *gin.Context) {
	projectID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
		return
	}
	cid := c.Param("cid")
	if cid == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "missing clarification id"})
		return
	}
	var req AnswerClarificationRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if req.Status == "" {
		if req.Answer != "" {
			req.Status = "answered"
		} else {
			req.Status = "open"
		}
	}

	ctx := c.Request.Context()
	project, err := h.store.GetProject(ctx, projectID)
	if err != nil || project == nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
		return
	}
	tenantID, terr := getTenantID(c)
	if terr != nil {
		tenantID = project.TenantID
	}

	// If the client didn't supply snapshot fields, fall back to whatever
	// the engine currently produces for this clarification id.
	if req.Question == "" || req.Source == "" {
		if prev, _ := h.store.GetClarificationByKey(ctx, projectID, cid); prev != nil {
			if req.Question == "" {
				req.Question = prev.Question
			}
			if req.Source == "" {
				req.Source = prev.Source
			}
			if req.Category == "" {
				req.Category = prev.Category
			}
			if len(req.NormReferences) == 0 {
				req.NormReferences = prev.NormReferences
			}
		}
	}

	now := time.Now().UTC()
	answeredAt := &now
	if req.Status != "answered" && req.Status != "not_relevant" {
		answeredAt = nil
	}
	in := iace.ClarificationRow{
		TenantID:         tenantID,
		ProjectID:        projectID,
		ClarificationKey: cid,
		Question:         req.Question,
		Source:           req.Source,
		Category:         req.Category,
		NormReferences:   req.NormReferences,
		Status:           req.Status,
		Answer:           req.Answer,
		Reasoning:        req.Reasoning,
		AssignedTo:       req.AssignedTo,
		AnsweredBy:       req.AnsweredBy,
		AnsweredAt:       answeredAt,
	}
	row, err := h.store.UpsertClarification(ctx, in)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"clarification_id": cid,
		"row":              row,
	})
}

// CommentRequest is the body for POST .../comment.
type CommentRequest struct {
	Author string `json:"author"`
	Body   string `json:"body"`
}

// PostClarificationComment handles POST /projects/:id/clarifications/:cid/comment.
func (h *IACEHandler) PostClarificationComment(c *gin.Context) {
	projectID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
		return
	}
	cid := c.Param("cid")
	var req CommentRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if req.Body == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "body required"})
		return
	}
	ctx := c.Request.Context()
	row, err := h.store.GetClarificationByKey(ctx, projectID, cid)
	if err != nil || row == nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "clarification not found — answer/assign it first to create the row"})
		return
	}
	comment, err := h.store.AddClarificationComment(ctx, row.ID, req.Author, req.Body)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, gin.H{"comment": comment})
}

// ListClarificationDetail handles GET /projects/:id/clarifications/:cid/detail
// and returns comments + history for one clarification.
func (h *IACEHandler) ListClarificationDetail(c *gin.Context) {
	projectID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
		return
	}
	cid := c.Param("cid")
	ctx := c.Request.Context()
	row, _ := h.store.GetClarificationByKey(ctx, projectID, cid)
	if row == nil {
		c.JSON(http.StatusOK, gin.H{"row": nil, "comments": []any{}, "history": []any{}})
		return
	}
	comments, _ := h.store.ListClarificationComments(ctx, row.ID)
	history, _ := h.store.ListClarificationHistory(ctx, row.ID)
	c.JSON(http.StatusOK, gin.H{
		"row":      row,
		"comments": comments,
		"history":  history,
	})
	_ = json.RawMessage{} // keep encoding/json import in case of future fields
}

// ExportClarificationsCSV handles GET /projects/:id/clarifications.csv.
// Returns the aggregated clarifications as a CSV for handover to the
// Anlagenbauer — one row per question with all referenced hazards and
// the current answer state.
func (h *IACEHandler) ExportClarificationsCSV(c *gin.Context) {
	projectID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
		return
	}
	ctx := c.Request.Context()
	project, err := h.store.GetProject(ctx, projectID)
	if err != nil || project == nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
		return
	}
	hazards, _ := h.store.ListHazards(ctx, projectID)
	answers, _ := readClarificationAnswers(project.Metadata)
	narrative := extractNarrativeFromMetadata(project.Metadata)
	hazardToPatterns := h.reconstructHazardPatterns(narrative, project.MachineType, hazards)
	manufHits := iace.LookupManufacturerFeaturesInText(narrative)
	clarifications := iace.BuildProjectClarifications(hazards, hazardToPatterns, manufHits, answers)

	sort.Slice(clarifications, func(i, j int) bool {
		if clarifications[i].Status != clarifications[j].Status {
			return clarifications[i].Status == "open"
		}
		return clarifications[i].Source < clarifications[j].Source
	})

	filename := fmt.Sprintf("klaerungen_%s_%s.csv", project.MachineName, time.Now().Format("2006-01-02"))
	filename = strings.ReplaceAll(filename, " ", "_")
	c.Header("Content-Type", "text/csv; charset=utf-8")
	c.Header("Content-Disposition", `attachment; filename="`+filename+`"`)
	// Excel-Erkennung: UTF-8 BOM voranstellen
	c.Writer.Write([]byte{0xEF, 0xBB, 0xBF})
	w := csv.NewWriter(c.Writer)
	w.Comma = ';'
	_ = w.Write([]string{
		"ID", "Quelle", "Kategorie", "Frage", "Status", "Antwort", "Begruendung",
		"Bearbeiter", "Beantwortet_am", "Anzahl_Gefaehrdungen", "Gefaehrdungen", "Norm_Referenzen",
	})
	for _, cl := range clarifications {
		_ = w.Write([]string{
			cl.ID,
			cl.Source,
			cl.Category,
			cl.Question,
			cl.Status,
			cl.Answer,
			cl.Reasoning,
			cl.AnsweredBy,
			cl.AnsweredAt,
			fmt.Sprintf("%d", len(cl.AffectedHazardIDs)),
			strings.Join(cl.AffectedHazardNames, " | "),
			strings.Join(cl.NormReferences, " | "),
		})
	}
	w.Flush()
}

// methodologyBlock returns the standardised methodology paragraph that
// must be printed at the start of every IACE risk-assessment report.
// Pure references to norm identifiers (no norm text) — kept here so
// the same wording appears in every export.
const methodologyBlock = `<section style="background:#f9fafb;border:1px solid #d1d5db;border-radius:6px;padding:12px;margin-bottom:18px;">
  <h3 style="font-size:11pt;margin:0 0 6px 0;">Methodik der Risikobeurteilung</h3>
  <ul style="margin:0;padding-left:18px;font-size:9.5pt;line-height:1.45;">
    <li>Gefaehrdungsidentifikation nach <strong>EN ISO 12100</strong>, Anhang B (Tabelle B.1) — Mechanik, Elektrik, Thermik, Laerm, Vibration, Strahlung, Materialien/Substanzen, Ergonomie.</li>
    <li>Bestimmung des erforderlichen Performance Levels (PLr) nach <strong>EN ISO 13849-1</strong>, Anhang A (Risikograph) aus S (Schwere), F (Haeufigkeit/Dauer) und P (Vermeidungsmoeglichkeit).</li>
    <li>Massnahmen-Hierarchie nach ISO 12100, Abschnitt 6: <strong>6.2 Inhaerent sichere Konstruktion</strong> (Design) &rarr; <strong>6.3 Technische Schutzmassnahmen</strong> (Protection) &rarr; <strong>6.4 Benutzerinformation</strong> (Information).</li>
    <li>Klaerungspunkte mit dem Anlagenbauer werden separat in der Klaerungs-Liste verwaltet (Audit-Trail mit Bearbeiter und Zeitstempel).</li>
  </ul>
</section>`

// ExportClarificationsHTML handles GET /projects/:id/clarifications.html
// and returns a print-friendly standalone HTML document that the browser
// can render to PDF (no server-side PDF dependency needed). The Bediener
// opens the link, hits Cmd-P / Strg-P and saves as PDF.
func (h *IACEHandler) ExportClarificationsHTML(c *gin.Context) {
	projectID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
		return
	}
	ctx := c.Request.Context()
	project, err := h.store.GetProject(ctx, projectID)
	if err != nil || project == nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
		return
	}
	hazards, _ := h.store.ListHazards(ctx, projectID)
	answers := map[string]iace.ClarificationAnswer{}
	if rows, _ := h.store.ListClarificationsForProject(ctx, projectID); rows != nil {
		for _, r := range rows {
			at := ""
			if r.AnsweredAt != nil {
				at = r.AnsweredAt.UTC().Format(time.RFC3339)
			}
			answers[r.ClarificationKey] = iace.ClarificationAnswer{
				Status: r.Status, Answer: r.Answer, Reasoning: r.Reasoning,
				AnsweredBy: r.AnsweredBy, AnsweredAt: at,
			}
		}
	}
	if legacy, _ := readClarificationAnswers(project.Metadata); len(legacy) > 0 {
		for k, v := range legacy {
			if _, ok := answers[k]; !ok {
				answers[k] = v
			}
		}
	}
	narrative := extractNarrativeFromMetadata(project.Metadata)
	hazardToPatterns := h.reconstructHazardPatterns(narrative, project.MachineType, hazards)
	manufHits := iace.LookupManufacturerFeaturesInText(narrative)
	cls := iace.BuildProjectClarifications(hazards, hazardToPatterns, manufHits, answers)
	sort.Slice(cls, func(i, j int) bool {
		if cls[i].Status != cls[j].Status {
			return cls[i].Status == "open"
		}
		return cls[i].Source < cls[j].Source
	})

	c.Header("Content-Type", "text/html; charset=utf-8")
	w := c.Writer
	fmt.Fprintf(w, `<!doctype html><html lang="de"><head><meta charset="utf-8">
<title>Klaerungen — %s</title>
<style>
@page { size: A4; margin: 18mm 15mm; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; font-size: 10pt; color: #111; line-height: 1.4; }
h1 { font-size: 16pt; margin: 0 0 4px 0; }
.sub { font-size: 9pt; color: #555; margin-bottom: 16px; }
.meta { font-size: 9pt; color: #444; margin-bottom: 12px; }
.bar { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 8pt; margin-right: 4px; }
.open { background: #fed7aa; color: #7c2d12; }
.done { background: #bbf7d0; color: #14532d; }
.gray { background: #e5e7eb; color: #374151; }
section { page-break-inside: avoid; margin-bottom: 14px; border: 1px solid #d1d5db; border-radius: 6px; padding: 10px 12px; }
section h2 { font-size: 10pt; margin: 0 0 2px 0; }
section .src { font-size: 8pt; color: #6b7280; margin-bottom: 6px; }
.q { font-weight: 600; font-size: 10.5pt; margin: 4px 0; }
.norm { font-size: 8pt; color: #555; }
.affected { font-size: 8pt; color: #555; margin: 4px 0; }
.answer { background: #ecfdf5; border: 1px solid #a7f3d0; padding: 6px 8px; border-radius: 4px; font-size: 9pt; margin-top: 6px; }
.signrow { margin-top: 30px; display: flex; gap: 40px; }
.signrow div { flex: 1; border-top: 1px solid #6b7280; padding-top: 4px; font-size: 8pt; color: #6b7280; }
@media print { .noprint { display: none; } }
.noprint { background: #fef9c3; border: 1px solid #fde047; padding: 6px 10px; border-radius: 4px; margin-bottom: 12px; font-size: 9pt; }
</style></head><body>
<div class="noprint">Tipp: Mit <kbd>Strg+P</kbd> / <kbd>Cmd+P</kbd> als PDF speichern.</div>
<h1>Klaerungsliste — %s</h1>
<div class="sub">Projekt-ID %s · Stand %s</div>
` + methodologyBlock + `
<div class="meta">
  <span class="bar open">%d offen</span>
  <span class="bar done">%d beantwortet</span>
  <span class="bar gray">%d gesamt</span>
</div>
`,
		htmlEscape(project.MachineName),
		htmlEscape(project.MachineName),
		project.ID.String(),
		time.Now().Format("2006-01-02 15:04"),
		countByStatus(cls, false), countByStatus(cls, true), len(cls),
	)
	for _, cl := range cls {
		statusCls := "open"
		statusLabel := "Offen"
		if cl.Status == "answered" {
			statusCls, statusLabel = "done", "Beantwortet"
		} else if cl.Status == "not_relevant" {
			statusCls, statusLabel = "gray", "Nicht relevant"
		} else if cl.Status == "in_progress" {
			statusCls, statusLabel = "open", "In Klaerung"
		}
		fmt.Fprintf(w, `<section><div class="src">%s · <span class="bar %s">%s</span></div>
<h2>%s</h2>
`,
			htmlEscape(cl.Source), statusCls, statusLabel,
			htmlEscape(cl.Question),
		)
		if len(cl.NormReferences) > 0 {
			fmt.Fprintf(w, `<div class="norm">Normen: %s</div>`, htmlEscape(strings.Join(cl.NormReferences, " | ")))
		}
		if len(cl.AffectedHazardNames) > 0 {
			fmt.Fprintf(w, `<div class="affected">Betrifft %d Gefaehrdung(en): %s</div>`,
				len(cl.AffectedHazardIDs),
				htmlEscape(strings.Join(cl.AffectedHazardNames, "; ")),
			)
		}
		if cl.Status == "answered" || cl.Status == "not_relevant" {
			fmt.Fprintf(w, `<div class="answer"><strong>Antwort (%s):</strong> %s`,
				htmlEscape(cl.Answer),
				htmlEscape(cl.Reasoning),
			)
			if cl.AnsweredBy != "" {
				ts := cl.AnsweredAt
				if len(ts) > 10 {
					ts = ts[:10]
				}
				fmt.Fprintf(w, ` <em>— %s, %s</em>`, htmlEscape(cl.AnsweredBy), htmlEscape(ts))
			}
			fmt.Fprintf(w, `</div>`)
		}
		fmt.Fprintf(w, `</section>`)
	}
	fmt.Fprintf(w, `<div class="signrow"><div>Anlagenbauer · Datum · Unterschrift</div><div>Bediener · Datum · Unterschrift</div></div>`)
	fmt.Fprintf(w, `</body></html>`)
}

func htmlEscape(s string) string {
	r := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;", `"`, "&quot;", `'`, "&#39;")
	return r.Replace(s)
}

func countByStatus(cls []iace.Clarification, answered bool) int {
	n := 0
	for _, c := range cls {
		isDone := c.Status == "answered" || c.Status == "not_relevant"
		if isDone == answered {
			n++
		}
	}
	return n
}
