package handlers

import (
	"fmt"
	"net/http"
	"strings"

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

// InitStep tracks progress of each initialization step.
type InitStep struct {
	Name    string `json:"name"`
	Status  string `json:"status"` // "done", "skipped", "error"
	Count   int    `json:"count,omitempty"`
	Details string `json:"details,omitempty"`
}

// InitializeProject handles POST /projects/:id/initialize
// Chains: parse narrative → create components → fire patterns →
// create hazards + measures + verification → suggest norms.
// Idempotent: skips steps that are already populated.
func (h *IACEHandler) InitializeProject(c *gin.Context) {
	projectID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
		return
	}

	tenantID, err := getTenantID(c)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		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
	}

	// Support ?force=true to clear existing hazards + mitigations before re-init
	forceReinit := c.Query("force") == "true"

	steps := make([]InitStep, 0, 6)

	// Phase 4: preserve the expert's component review across a force re-seed.
	// Captured (by normalised name) before deletion, re-applied after the fresh
	// parse so manual presence moves, CE marks and safety flags are not wiped.
	componentDecisions := make(map[string]componentReviewDecision)

	// ── Step 0 (optional): Clear existing data for force re-init ──
	if forceReinit {
		cleared := 0
		if mits, _ := h.store.ListMitigationsByProject(ctx, projectID); len(mits) > 0 {
			for _, m := range mits {
				_ = h.store.DeleteMitigation(ctx, m.ID)
				cleared++
			}
		}
		if hazards, _ := h.store.ListHazards(ctx, projectID); len(hazards) > 0 {
			for _, hz := range hazards {
				_ = h.store.DeleteHazard(ctx, hz.ID)
				cleared++
			}
		}
		// Also clear components so the fresh narrative parse re-creates them —
		// but first remember the expert's review so it survives the re-seed.
		if comps, _ := h.store.ListComponents(ctx, projectID); len(comps) > 0 {
			for _, cp := range comps {
				componentDecisions[iace.NormalizeDEPublic(cp.Name)] = componentReviewDecision{
					presence:       cp.PresenceStatus,
					ceMarked:       cp.CEMarked,
					safetyRelevant: cp.IsSafetyRelevant,
				}
				_ = h.store.DeleteComponent(ctx, cp.ID)
				cleared++
			}
		}
		steps = append(steps, InitStep{Name: "Alte Daten geloescht", Status: "done", Count: cleared})
	}

	// ── Step 1: Extract narrative from limits_form ──
	narrativeText := extractNarrativeFromMetadata(project.Metadata)
	if narrativeText == "" {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": "Grenzen-Formular ist leer. Bitte zuerst die Maschinenbeschreibung ausfuellen.",
		})
		return
	}

	// ── Step 2: Parse narrative deterministically ──
	parseResult := iace.ParseNarrative(narrativeText, project.MachineType)
	steps = append(steps, InitStep{
		Name:    "Narrative analysiert",
		Status:  "done",
		Count:   len(parseResult.Components),
		Details: fmt.Sprintf("%d Komponenten, %d Energiequellen, %d Tags", len(parseResult.Components), len(parseResult.EnergySources), len(parseResult.CustomTags)),
	})

	// ── Step 3: Create components (skip if already exist) ──
	existingComps, _ := h.store.ListComponents(ctx, projectID)
	compStep := InitStep{Name: "Komponenten erstellt", Status: "skipped"}
	if len(existingComps) == 0 && len(parseResult.Components) > 0 {
		created := 0
		for _, comp := range parseResult.Components {
			compType := deriveComponentType(comp.Tags)
			dec, hasDecision := componentDecisions[iace.NormalizeDEPublic(comp.NameDE)]
			_, cerr := h.store.CreateComponent(ctx, iace.CreateComponentRequest{
				ProjectID:        projectID,
				Name:             comp.NameDE,
				ComponentType:    compType,
				Description:      "Auto-erkannt aus Maschinenbeschreibung (" + comp.MatchedOn + ")",
				PresenceStatus:   resolvePresence(dec, hasDecision, comp.Negated),
				CEMarked:         dec.ceMarked,
				IsSafetyRelevant: dec.safetyRelevant,
			})
			if cerr == nil {
				created++
			}
		}
		compStep = InitStep{Name: "Komponenten erstellt", Status: "done", Count: created}
	} else if len(existingComps) > 0 {
		compStep.Details = "Bereits vorhanden"
		compStep.Count = len(existingComps)
	}
	steps = append(steps, compStep)

	// ── Step 4: Fire pattern engine ──
	var componentIDs, energyIDs []string
	for _, comp := range parseResult.Components {
		dec, hasDecision := componentDecisions[iace.NormalizeDEPublic(comp.NameDE)]
		if resolvePresence(dec, hasDecision, comp.Negated) != iace.PresencePresent {
			continue // negated/deleted (engine verdict OR expert decision) → no matching
		}
		componentIDs = append(componentIDs, comp.LibraryID)
	}
	for _, e := range parseResult.EnergySources {
		energyIDs = append(energyIDs, e.SourceID)
	}

	operationalStates := mergeStringSlices(parseResult.OperationalStates, extractOperationalStatesFromMetadata(project.Metadata))
	machineTypes := extractIndustrySectorsFromMetadata(project.Metadata)
	// The project's own machine type MUST reach the engine's machine-type gate,
	// otherwise (empty input) every pattern scoped to a foreign machine class
	// fires — press, CNC, excavator, crane, road-roller, medical ... For a lift
	// this floods the result with cross-domain nonsense. With it set, those
	// patterns are gated out; only this machine class + ungated patterns remain.
	if project.MachineType != "" {
		machineTypes = append(machineTypes, project.MachineType)
	}

	engine := iace.NewPatternEngine()
	matchOutput := engine.Match(iace.MatchInput{
		ComponentLibraryIDs: componentIDs,
		EnergySourceIDs:     energyIDs,
		LifecyclePhases:     withUniversalLifecycles(parseResult.LifecyclePhases),
		CustomTags:          parseResult.CustomTags,
		OperationalStates:   operationalStates,
		StateTransitions:    parseResult.StateTransitions,
		HumanRoles:          parseResult.Roles,
		MachineTypes:        machineTypes,
	})
	steps = append(steps, InitStep{
		Name:   "Patterns abgeglichen",
		Status: "done",
		Count:  len(matchOutput.MatchedPatterns),
	})

	// ── Step 5: Create hazards from matched patterns (skip if exist) ──
	existingHazards, _ := h.store.ListHazards(ctx, projectID)
	hazardStep := InitStep{Name: "Gefaehrdungen erstellt", Status: "skipped"}
	hazardIDsByCategory := make(map[string][]uuid.UUID)
	hazardPatternMeasures := make(map[uuid.UUID][]string)

	if len(existingHazards) == 0 && len(matchOutput.MatchedPatterns) > 0 {
		allComps, _ := h.store.ListComponents(ctx, projectID)
		// Hazards only attach to PRESENT components — negated/deleted ones are
		// surfaced for review but never own a generated hazard.
		comps := make([]iace.Component, 0, len(allComps))
		for _, c := range allComps {
			if c.PresenceStatus == iace.PresencePresent || c.PresenceStatus == "" {
				comps = append(comps, c)
			}
		}
		var defaultCompID uuid.UUID
		compByName := make(map[string]uuid.UUID)
		if len(comps) > 0 {
			defaultCompID = comps[0].ID
			for _, c := range comps {
				compByName[iace.NormalizeDEPublic(c.Name)] = c.ID
			}
		}

		// Build component name set for relevance filtering
		compNames := make([]string, 0, len(comps))
		for name := range compByName {
			compNames = append(compNames, name)
		}

		created := 0
		seenCatZone := make(map[string]uuid.UUID) // dedupKey → hazardID
		catCount := make(map[string]int)
		for _, mp := range matchOutput.MatchedPatterns {
			// Narrative relevance filter
			if !iace.IsPatternRelevant(mp, narrativeText, compNames) {
				continue
			}

			for _, cat := range mp.HazardCats {
				// Native cyber/AI categories (frontend groups I+J) belong to the
				// CRA module, not the traditional CE (ISO 12100) hazard log.
				// Enforced centrally here so it holds for EVERY project.
				if isCyberSecurityCategory(cat) {
					fmt.Printf("CYBER-SKIP: cat=%s pattern=%s — routed to CRA module\n", cat, mp.PatternID)
					continue
				}
				maxForCat := categoryHazardCap(cat, len(comps))
				if catCount[cat] >= maxForCat {
					continue
				}

				zoneKey := normalizeZoneKey(mp.ZoneDE)
				if zoneKey == "" {
					zoneKey = mp.PatternID
				}
				dedupKey := cat + ":" + zoneKey

				// If this dedupKey already exists but current pattern has
				// SuggestedMeasureIDs, add them to the existing hazard
				if existingHzID, exists := seenCatZone[dedupKey]; exists {
					if len(mp.SuggestedMeasureIDs) > 0 {
						existing := hazardPatternMeasures[existingHzID]
						hazardPatternMeasures[existingHzID] = append(existing, mp.SuggestedMeasureIDs...)
					}
					continue
				}

				name := mp.PatternName
				if name == "" {
					name = cat
				}
				if mp.ZoneDE != "" && !containsSubstring(name, mp.ZoneDE) {
					name = name + " (" + mp.ZoneDE + ")"
				}

				compID := pickComponentForPattern(mp.MatchedTags, mp.ZoneDE, parseResult.Components, compByName, defaultCompID)

				// Join all applicable lifecycles as comma-separated string
				lifecycleStr := strings.Join(mp.ApplicableLifecycles, ",")

				// Phase 2: clarification questions are no longer embedded
				// in the hazard description — they live in the dedicated
				// /clarifications API and the UI loads them on demand.
				// The hazard description stays clean and focused on the
				// scenario itself. Only the aggregated norm-references
				// block is appended below for an at-a-glance audit trail.
				desc := mp.ScenarioDE
				// BreakPilot's OWN risk model (NOT a norm reproduction):
				// severity + frequency from the pattern defaults; probability
				// (W) and avoidance (P) from public accident-statistics anchors
				// (see iace/risk_estimation.go + DATA_SOURCES.md). No EN ISO
				// 13849-1 risk-graph table or parameter binning is reproduced.
				if mp.DefaultSeverity > 0 && mp.DefaultExposure > 0 {
					s := iace.EstimateSeverity(mp.HazardCats, mp.ScenarioDE, mp.DefaultSeverity)
					w := iace.EstimateProbabilityW(mp.HazardCats, mp.ScenarioDE)
					p := iace.EstimateAvoidabilityP(mp.HazardCats, mp.ScenarioDE)
					_, level := iace.EstimateRiskLevel(s, mp.DefaultExposure, w, p)
					desc += fmt.Sprintf("\n\nRisikoeinschaetzung (BreakPilot-Modell): S%d · F%d · W%d · P%d → Risiko: %s",
						s, mp.DefaultExposure, w, p, level)
				}
				if mp.ISO12100Section != "" {
					desc += "\n\nKlassifikation: EN ISO 12100 Abschnitt " + mp.ISO12100Section
				}

				hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{
					ProjectID:      projectID,
					ComponentID:    compID,
					Name:           name,
					Description:    desc,
					Category:       cat,
					Scenario:       mp.ScenarioDE,
					Function:       iace.EncodeOpStates(mp.OperationalStates),
					LifecyclePhase: lifecycleStr,
					TriggerEvent:   mp.TriggerDE,
					PossibleHarm:   mp.HarmDE,
					AffectedPerson: mp.AffectedDE,
					HazardousZone:  mp.ZoneDE,
				})
				if cerr == nil {
					created++
					catCount[cat]++
					seenCatZone[dedupKey] = hz.ID
					hazardIDsByCategory[cat] = append(hazardIDsByCategory[cat], hz.ID)
					if len(mp.SuggestedMeasureIDs) > 0 {
						hazardPatternMeasures[hz.ID] = mp.SuggestedMeasureIDs
					}
					// E1: one hazard per pattern — keep only the primary (first
					// eligible) category; a secondary category would be the same
					// scenario+zone under a different label (cross-category duplicate).
					break
				}
			}
		}
		hazardStep = InitStep{Name: "Gefaehrdungen erstellt", Status: "done", Count: created}
	} else if len(existingHazards) > 0 {
		hazardStep.Details = "Bereits vorhanden"
		hazardStep.Count = len(existingHazards)
		for _, eh := range existingHazards {
			hazardIDsByCategory[eh.Category] = append(hazardIDsByCategory[eh.Category], eh.ID)
		}
	}
	steps = append(steps, hazardStep)

	// ── Step 6: Create mitigations ──
	existingMits, _ := h.store.ListMitigationsByProject(ctx, projectID)
	mitStep := InitStep{Name: "Massnahmen erstellt", Status: "skipped"}

	if len(existingMits) == 0 && len(hazardIDsByCategory) > 0 {
		measureLib := iace.GetProtectiveMeasureLibrary()
		measureByID := make(map[string]iace.ProtectiveMeasureEntry, len(measureLib))
		measuresByCat := make(map[string][]iace.ProtectiveMeasureEntry)
		for _, m := range measureLib {
			measureByID[m.ID] = m
			measuresByCat[m.HazardCategory] = append(measuresByCat[m.HazardCategory], m)
		}

		created := 0
		const maxMitigationsPerHazard = 5

		// Build a flat list of all hazard IDs for iteration
		var allHazardIDs []uuid.UUID
		hazardCatByID := make(map[uuid.UUID]string)
		for cat, ids := range hazardIDsByCategory {
			for _, id := range ids {
				allHazardIDs = append(allHazardIDs, id)
				hazardCatByID[id] = cat
			}
		}

		// For each hazard: only pattern-specific SuggestedMeasureIDs are
		// used, FILTERED by category. Measures whose HazardCategory is
		// incompatible with the pattern's accepted set are skipped with a
		// MEASURE-SKIP log entry. There is NO category fallback any more —
		// if the pattern author left a hazard without applicable measures,
		// the hazard is created with zero mitigations and the operator must
		// consult an expert. This is the only honest answer: silently
		// inventing generic defaults (the previous behavior) produced
		// nonsense like "Rotationsbewegung vermeiden" for a sharp-edge
		// hazard. See feat/iace-measure-category-filter for context.
		_ = measuresByCat // retained for backwards-compat read by other code paths
		_ = patternCatToMeasureCat
		zeroMitigationHazards := 0
		for _, hazID := range allHazardIDs {
			hazCat := hazardCatByID[hazID]
			accepted := acceptableMeasureCategories(hazCat)
			added := 0
			// Aggregate norm references across all kept mitigations for this
			// hazard so we can attach a single "Referenzierte Normen" line
			// to the hazard description below.
			var hazardNorms []string
			seenNorm := map[string]bool{}

			if patternMIDs, ok := hazardPatternMeasures[hazID]; ok {
				for _, mid := range patternMIDs {
					if added >= maxMitigationsPerHazard {
						break
					}
					entry, ok := measureByID[mid]
					if !ok {
						continue
					}
					if !isCategoryCompatible(entry.HazardCategory, accepted) {
						fmt.Printf("MEASURE-SKIP: pattern-cat=%s acceptable=%v but mid=%s has cat=%s (%q) — skipping mismatch\n",
							hazCat, keysOf(accepted), mid, entry.HazardCategory, entry.Name)
						continue
					}

					rt := iace.ReductionType(entry.ReductionType)
					if rt == "" {
						rt = iace.ReductionTypeInformation
					}
					mitDesc := entry.Description
					if len(entry.NormReferences) > 0 {
						mitDesc += "\n\nNormen: " + strings.Join(entry.NormReferences, " | ")
						for _, n := range entry.NormReferences {
							if !seenNorm[n] {
								seenNorm[n] = true
								hazardNorms = append(hazardNorms, n)
							}
						}
					}
					_, cerr := h.store.CreateMitigation(ctx, iace.CreateMitigationRequest{
						HazardID: hazID, ReductionType: rt,
						Name: entry.Name, Description: mitDesc,
					})
					if cerr != nil {
						fmt.Printf("MEASURE-ERROR: mid=%s name=%s err=%v\n", mid, entry.Name, cerr)
					} else {
						created++
						added++
					}
				}
			}
			// Append the aggregated norm list to the hazard so the UI shows
			// a single "Referenzierte Normen" panel per hazard.
			if len(hazardNorms) > 0 {
				if existing, getErr := h.store.GetHazard(ctx, hazID); getErr == nil && existing != nil {
					if !strings.Contains(existing.Description, "Referenzierte Normen:") {
						newDesc := existing.Description + "\n\nReferenzierte Normen: " + strings.Join(hazardNorms, " | ")
						_, _ = h.store.UpdateHazard(ctx, hazID, map[string]interface{}{
							"description": newDesc,
						})
					}
				}
			}

			if added == 0 {
				zeroMitigationHazards++
				fmt.Printf("COVERAGE-GAP: hazard %s (cat=%s) has no pattern-specific measures — operator must consult expert\n",
					hazID, hazCat)
			}
		}
		if zeroMitigationHazards > 0 {
			fmt.Printf("COVERAGE-GAP-SUMMARY: %d/%d hazards in this project have no mitigations and need expert review\n",
				zeroMitigationHazards, len(allHazardIDs))
		}
		patternMeasureCount := 0
		for _, mids := range hazardPatternMeasures {
			patternMeasureCount += len(mids)
		}
		mitStep = InitStep{Name: "Massnahmen erstellt", Status: "done", Count: created,
			Details: fmt.Sprintf("%d pattern-spezifisch fuer %d Hazards", patternMeasureCount, len(hazardPatternMeasures))}
	} else if len(existingMits) > 0 {
		mitStep.Details = "Bereits vorhanden"
		mitStep.Count = len(existingMits)
	}
	steps = append(steps, mitStep)

	// ── Step 7: Suggest norms ──
	var hazardCats []string
	for cat := range hazardIDsByCategory {
		hazardCats = append(hazardCats, cat)
	}
	normResult := iace.SuggestNorms(project.MachineType, hazardCats, parseResult.CustomTags)
	normCount := 0
	if normResult != nil {
		normCount = len(normResult.ANorms) + len(normResult.B1Norms) + len(normResult.B2Norms) + len(normResult.CNorms)
	}
	steps = append(steps, InitStep{Name: "Normen vorgeschlagen", Status: "done", Count: normCount})

	// ── Audit trail ──
	h.store.AddAuditEntry(ctx, projectID, "project_initialization", projectID,
		iace.AuditActionCreate, tenantID.String(), nil,
		mustMarshalJSON(map[string]interface{}{"steps": steps}),
	)

	// Summary is built from the NAMED step variables, not positional steps[]
	// indices: with ?force=true an extra "Alte Daten geloescht" step is prepended,
	// which previously shifted steps[1..5] by one and mislabeled every count
	// (e.g. reporting matched-patterns as "hazards"). These vars are stable.
	c.JSON(http.StatusOK, gin.H{
		"project_id": projectID.String(),
		"steps":      steps,
		"summary": gin.H{
			"components":  compStep.Count,
			"patterns":    len(matchOutput.MatchedPatterns),
			"hazards":     hazardStep.Count,
			"mitigations": mitStep.Count,
			"norms":       normCount,
		},
	})
}
