package iace

// Minimum-distance library — Task #18.
//
// Anchor source: OSHA 29 CFR 1910 Subpart O (US Federal Public Domain,
// 17 U.S.C. §105). The values below are reproduced verbatim from the
// Federal Code; conversions to metric are mathematical and carry no
// copyright. Engineering rounding to safe-side mm values is BreakPilot's
// recommendation and labelled as such.
//
// EU norm equivalents (EN ISO 13857, EN 349, EN 13855, EN 1010) are
// referenced by identifier only — no values are reproduced, because
// DIN/Beuth retain copyright on the wording. The DINComparisonNote
// field carries a human-curated judgement on whether the EU norm is
// stricter / looser / equivalent — this is a qualitative observation
// about a publicly available document, not a copy of its text.
//
// See LICENSE_RULES.md and project_attribution_strategy.md for the
// licensing logic. The OSHA values are R1 (verbatim public domain);
// the recommended metric values are BreakPilot engineering output (R3
// own-work). DIN references are R3 identifier-only.

// MinimumDistanceUnit denotes the original unit system of the source.
type MinimumDistanceUnit string

const (
	UnitInch  MinimumDistanceUnit = "inch"
	UnitFoot  MinimumDistanceUnit = "foot"
	UnitMeter MinimumDistanceUnit = "meter"
	UnitMM    MinimumDistanceUnit = "mm"
)

// MinimumDistance is the data contract for a single safety-distance rule.
// It can be (a) a fixed gap value, (b) a distance range, or (c) a formula
// like OSHA's Ds = 63 in/s × Ts (hand-speed constant).
type MinimumDistance struct {
	ID string `json:"id"` // MD_OSHA_001
	// Source identifier — full CFR citation or norm reference.
	SourceCFR   string `json:"source_cfr,omitempty"`   // "29 CFR §1910.217(c)(1)(i)"
	SourceTable string `json:"source_table,omitempty"` // "Table O-10"
	License     string `json:"license"`                // "US Federal Public Domain"
	LicenseRule int    `json:"license_rule"`           // 1 / 2 / 3 (see LICENSE_RULES.md)

	// Original verbatim value in the source's own unit.
	OriginalUnit  MinimumDistanceUnit `json:"original_unit"`
	OriginalValue float64             `json:"original_value,omitempty"`
	OriginalMin   float64             `json:"original_min,omitempty"`
	OriginalMax   float64             `json:"original_max,omitempty"`

	// Exact conversion to mm — no engineering rounding.
	ExactMM    float64 `json:"exact_mm,omitempty"`
	ExactMinMM float64 `json:"exact_min_mm,omitempty"`
	ExactMaxMM float64 `json:"exact_max_mm,omitempty"`

	// Engineering-recommended metric value with safe-side rounding.
	// For minimum distances: rounded up. For maximum opening widths:
	// rounded down.
	RecommendedMM    int    `json:"recommended_mm,omitempty"`
	RecommendedMinMM int    `json:"recommended_min_mm,omitempty"`
	RecommendedMaxMM int    `json:"recommended_max_mm,omitempty"`
	RoundingNote     string `json:"rounding_note,omitempty"`

	// Optional formula constant (e.g. OSHA hand-speed 63 in/s).
	FormulaInchPerSecond float64 `json:"formula_inch_per_second,omitempty"`
	FormulaMMPerSecond   float64 `json:"formula_mm_per_second,omitempty"`
	FormulaDescription   string  `json:"formula_description,omitempty"`

	Context    string   `json:"context"`               // "Point of Operation Guarding mechanical presses"
	BodyPart   string   `json:"body_part,omitempty"`   // "finger" / "hand" / "head" / "foot" / "body"
	HazardTags []string `json:"hazard_tags,omitempty"` // [crush_point, cutting_part, ...]

	// EU norm cross-reference — IDENTIFIER ONLY, no values reproduced.
	EUNormHints []EUNormHint `json:"eu_norm_hints,omitempty"`
}

// EUNormHint references an EU standard by identifier without reproducing
// any value or text from it. The DINComparisonNote is a human-curated
// qualitative judgement (stricter / equivalent / looser) — not a copy.
type EUNormHint struct {
	Norm              string `json:"norm"`              // "EN ISO 13857"
	Section           string `json:"section,omitempty"` // "Tab. 4, Schutz gegen Hineingreifen"
	DINComparisonNote string `json:"din_comparison_note,omitempty"`
}

// GetOSHAMinimumDistances returns the verbatim OSHA values for
// machine-guarding distances. All values are US Federal Public Domain
// (17 U.S.C. §105). Engineering rounding is BreakPilot's safe-side
// recommendation; OSHA values themselves are unchanged.
func GetOSHAMinimumDistances() []MinimumDistance {
	return []MinimumDistance{
		// OSHA Table O-10 row 1 — verbatim values, mathematical conversion,
		// safe-side rounded engineering recommendation.
		{
			ID:           "MD_OSHA_O10_R1",
			SourceCFR:    "29 CFR §1910.217(c)(1)(i)",
			SourceTable:  "Table O-10 row 1",
			License:      "US Federal Public Domain (17 U.S.C. §105)",
			LicenseRule:  1,
			OriginalUnit: UnitInch,
			OriginalMin:  0.5, OriginalMax: 1.5, OriginalValue: 0.25,
			ExactMinMM: 12.7, ExactMaxMM: 38.1, ExactMM: 6.35,
			RecommendedMinMM: 15, RecommendedMaxMM: 40, RecommendedMM: 6,
			RoundingNote: "Distance auf 5-mm-Raster aufgerundet, opening auf 1-mm-Raster abgerundet (konservativ in beide Richtungen).",
			Context:      "Point-of-Operation Guarding bei mechanischen Pressen",
			BodyPart:     "finger",
			HazardTags:   []string{"crush_point", "cutting_part"},
			EUNormHints: []EUNormHint{
				{Norm: "EN ISO 13857", Section: "Tab. 4 (Hineingreifen)",
					DINComparisonNote: "Andere Methodik (Reichweitenmodell). Unabhaengig pruefen — Werte koennen abweichen."},
			},
		},
		// OSHA Table O-10 row 4 — used as a worked example in the strategy
		// discussion. Distance 3.5-5.5 in, opening max 5/8 in.
		{
			ID:           "MD_OSHA_O10_R4",
			SourceCFR:    "29 CFR §1910.217(c)(1)(i)",
			SourceTable:  "Table O-10 row 4",
			License:      "US Federal Public Domain (17 U.S.C. §105)",
			LicenseRule:  1,
			OriginalUnit: UnitInch,
			OriginalMin:  3.5, OriginalMax: 5.5, OriginalValue: 0.625,
			ExactMinMM: 88.9, ExactMaxMM: 139.7, ExactMM: 15.875,
			RecommendedMinMM: 90, RecommendedMaxMM: 140, RecommendedMM: 15,
			RoundingNote: "Distance 88.9→90 (+1.1 mm), 139.7→140 (+0.3 mm) aufgerundet; Opening 15.875→15 (-0.875 mm) abgerundet.",
			Context:      "Point-of-Operation Guarding bei mechanischen Pressen",
			BodyPart:     "finger",
			HazardTags:   []string{"crush_point", "cutting_part"},
			EUNormHints: []EUNormHint{
				{Norm: "EN ISO 13857", Section: "Tab. 4 (Hineingreifen)",
					DINComparisonNote: "Andere Methodik (Reichweitenmodell). Compliance-Annotation pflegen."},
			},
		},
		// OSHA §1910.212(a)(5) — fan blade guards. Verbatim 1/2 inch.
		{
			ID:            "MD_OSHA_212_FAN",
			SourceCFR:     "29 CFR §1910.212(a)(5)",
			License:       "US Federal Public Domain (17 U.S.C. §105)",
			LicenseRule:   1,
			OriginalUnit:  UnitInch,
			OriginalValue: 0.5,
			ExactMM:       12.7,
			RecommendedMM: 12,
			RoundingNote:  "Luefterblatt-Schutzgitter: max. Spaltoeffnung 1/2 in = 12.7 mm. Konservativ auf 12 mm abgerundet.",
			Context:       "Lüfterblätter unter 7 ft (2.13 m) Höhe",
			BodyPart:      "finger",
			HazardTags:    []string{"rotating_part", "cutting_part"},
			EUNormHints: []EUNormHint{
				{Norm: "EN ISO 13857", Section: "Tab. 4",
					DINComparisonNote: "DIN-Wert pruefen."},
			},
		},
		// OSHA §1910.217 Hand-Speed Constant — formula Ds = 63 in/s × Ts
		{
			ID:                   "MD_OSHA_217_PSDI",
			SourceCFR:            "29 CFR §1910.217 (Ds = 63 in/s × Ts)",
			License:              "US Federal Public Domain (17 U.S.C. §105)",
			LicenseRule:          1,
			OriginalUnit:         UnitInch,
			FormulaInchPerSecond: 63.0,
			FormulaMMPerSecond:   1600.2,
			FormulaDescription: "Hand-Speed-Konstante 63 in/s ≈ 1600 mm/s. " +
				"Ds (Mindestabstand) = 63 × Ts (Stoppzeit Presse in Sekunden).",
			Context:    "PSDI Presence-Sensing Device Initiation und Two-Hand-Trip",
			BodyPart:   "hand",
			HazardTags: []string{"crush_point", "high_speed"},
			EUNormHints: []EUNormHint{
				{Norm: "EN 13855", Section: "Sicherheitsabstaende",
					DINComparisonNote: "EN 13855 nutzt andere Konstante (1600 mm/s ≈ identisch); EU-Norm unabhaengig pruefen."},
			},
		},
	}
}

// MinimumDistanceNote is the ready-to-print licensing posture for the
// minimum-distance reference — shown by the API so an auditor sees WHY the
// OSHA values may be reproduced while the EU norms are reference-only.
const MinimumDistanceNote = "OSHA-Werte (29 CFR 1910) sind US-Public-Domain " +
	"(17 U.S.C. §105) und werden verbatim wiedergegeben; die mm-Umrechnung ist " +
	"mathematisch, die sicherheitsseitige Rundung ist BreakPilot-Empfehlung. " +
	"EU-Normen (EN ISO 13857/13854/13855, EN 349) werden nur per Kennung " +
	"referenziert — keine Werte reproduziert."

// GetMinimumDistanceByID returns the OSHA distance entry with the given ID.
func GetMinimumDistanceByID(id string) (MinimumDistance, bool) {
	for _, md := range GetOSHAMinimumDistances() {
		if md.ID == id {
			return md, true
		}
	}
	return MinimumDistance{}, false
}

// MeasureDistanceLink connects a protective measure to the OSHA distance
// entries that anchor it. Relation makes the nature of the link explicit so
// the join is honest rather than implying every measure's prose IS the OSHA
// value:
//   - "value_source"           — the OSHA value is the source the measure's own
//     mm figure is derived from (it appears in the measure prose).
//   - "public_domain_crossref" — the measure is dimensioned by an EU norm; the
//     OSHA entry is offered as the public-domain pendant for independent check.
type MeasureDistanceLink struct {
	MeasureID   string   `json:"measure_id"`
	MeasureName string   `json:"measure_name,omitempty"` // resolved from the library for name-based UI matching
	DistanceIDs []string `json:"distance_ids"`
	Relation    string   `json:"relation"`
	Note        string   `json:"note,omitempty"`
}

const (
	LinkValueSource = "value_source"
	LinkCrossRef    = "public_domain_crossref"
)

// AllMeasureDistanceLinks returns the curated measure→OSHA-distance links.
// Conservative on purpose: only measures whose CONTEXT genuinely matches an
// OSHA entry are linked. Measures whose "OSHA" citation is loose or carries an
// ISO value (e.g. M340 robot teach speed, M368 air-receiver wall) are NOT
// linked — that would imply a public-domain anchor that does not exist.
func AllMeasureDistanceLinks() []MeasureDistanceLink {
	links := []MeasureDistanceLink{
		{
			MeasureID:   "M600",
			DistanceIDs: []string{"MD_OSHA_217_PSDI"},
			Relation:    LinkValueSource,
			Note:        "Hand-Speed-Konstante 1.600 mm/s (63 in/s) ist die Obergrenze, aus der die Kriechgeschwindigkeit am Endanschlag abgeleitet ist.",
		},
		{
			MeasureID:   "M254",
			DistanceIDs: []string{"MD_OSHA_O10_R1", "MD_OSHA_O10_R4"},
			Relation:    LinkCrossRef,
			Note:        "OSHA Table O-10 (Point-of-Operation an mechanischen Pressen) als Public-Domain-Pendant zur ISO-13855-Methode — Werte eigenstaendig pruefen.",
		},
		{
			MeasureID:   "M065",
			DistanceIDs: []string{"MD_OSHA_212_FAN"},
			Relation:    LinkCrossRef,
			Note:        "OSHA §1910.212(a)(5) Luefterschutz (max. 12 mm Spaltoeffnung) als Public-Domain-Pendant zu ISO 13857.",
		},
	}
	// Enrich each link with the measure name so a name-based UI (persisted
	// mitigations carry the name, not the ID) can match without a second lookup.
	lib := GetProtectiveMeasureLibrary()
	for i := range links {
		for _, m := range lib {
			if m.ID == links[i].MeasureID {
				links[i].MeasureName = m.Name
				break
			}
		}
	}
	return links
}

// LinksForMeasure returns the distance links declared for one measure.
func LinksForMeasure(measureID string) []MeasureDistanceLink {
	var out []MeasureDistanceLink
	for _, l := range AllMeasureDistanceLinks() {
		if l.MeasureID == measureID {
			out = append(out, l)
		}
	}
	return out
}

// MinimumDistancesForMeasure resolves the OSHA distance entries linked to a
// protective measure. This is the join that finally lets the OSHA mm values
// "flow into" the measures (read-side), without mutating the measure object.
func MinimumDistancesForMeasure(measureID string) []MinimumDistance {
	var out []MinimumDistance
	for _, l := range LinksForMeasure(measureID) {
		for _, id := range l.DistanceIDs {
			if md, ok := GetMinimumDistanceByID(id); ok {
				out = append(out, md)
			}
		}
	}
	return out
}

// MeasureIDByName resolves a protective-measure name to its catalog ID. Measure
// names are unique across the library (verified by test), so a PERSISTED
// mitigation — which stores the measure name verbatim but NOT the catalog ID —
// can still be joined to its OSHA distance link without a DB schema change or a
// re-seed. This is the bridge that lets a project's measures surface the anchor.
func MeasureIDByName(name string) (string, bool) {
	for _, m := range GetProtectiveMeasureLibrary() {
		if m.Name == name {
			return m.ID, true
		}
	}
	return "", false
}

// MinimumDistancesForMeasureName resolves OSHA distances by measure NAME.
func MinimumDistancesForMeasureName(name string) []MinimumDistance {
	if id, ok := MeasureIDByName(name); ok {
		return MinimumDistancesForMeasure(id)
	}
	return nil
}

// LinksForMeasureName resolves distance links by measure NAME.
func LinksForMeasureName(name string) []MeasureDistanceLink {
	if id, ok := MeasureIDByName(name); ok {
		return LinksForMeasure(id)
	}
	return nil
}
