// Package apicontract implements the service-contract mechanics an
// architect designs around: protobuf-style schema compatibility checking,
// idempotency-key dedup (safe retries of non-idempotent operations), and
// opaque, tamper-evident pagination cursors. Stdlib-only.
package apicontract

import (
	"fmt"
	"sort"
)

// Field is one field in a message contract (protobuf-style: identified by
// a stable Tag number, not by position).
type Field struct {
	Tag      int
	Name     string
	Type     string
	Required bool
}

// Severity classifies a contract change.
type Severity int

const (
	Safe     Severity = iota // compatible: consumers keep working
	Warning                  // source-level change; wire-compatible but needs care
	Breaking                 // incompatible: will break consumers / corrupt the wire
)

func (s Severity) String() string { return [...]string{"SAFE", "WARNING", "BREAKING"}[s] }

// Change is one detected difference between two schema versions.
type Change struct {
	Tag      int
	Severity Severity
	Reason   string
}

// Check compares an old schema to a new one and returns the compatibility
// changes, applying the rules a tool like `buf breaking` enforces:
//
//   - changing a field's TYPE on a tag      -> BREAKING (wire-incompatible)
//   - making an optional field REQUIRED     -> BREAKING (old producers may omit it)
//   - ADDING a required field               -> BREAKING (old messages lack it)
//   - REMOVING a field                      -> WARNING  (reserve the tag so it's never reused!)
//   - RENAMING a field (same tag/type)      -> WARNING  (source change; wire OK)
//   - ADDING an optional field              -> SAFE
//   - making a required field OPTIONAL      -> SAFE     (loosening)
//
// Results are sorted by tag for determinism.
func Check(oldSchema, newSchema []Field) []Change {
	oldByTag := index(oldSchema)
	newByTag := index(newSchema)
	var out []Change

	for tag, o := range oldByTag {
		n, ok := newByTag[tag]
		if !ok {
			out = append(out, Change{tag, Warning,
				fmt.Sprintf("field %q removed on tag %d (reserve the tag to prevent reuse)", o.Name, tag)})
			continue
		}
		if o.Type != n.Type {
			out = append(out, Change{tag, Breaking,
				fmt.Sprintf("type changed on tag %d: %s -> %s (wire-incompatible)", tag, o.Type, n.Type)})
		}
		if !o.Required && n.Required {
			out = append(out, Change{tag, Breaking,
				fmt.Sprintf("field %q on tag %d became required (old producers may omit it)", n.Name, tag)})
		}
		if o.Required && !n.Required {
			out = append(out, Change{tag, Safe,
				fmt.Sprintf("field %q on tag %d relaxed to optional", n.Name, tag)})
		}
		if o.Name != n.Name && o.Type == n.Type {
			out = append(out, Change{tag, Warning,
				fmt.Sprintf("field renamed on tag %d: %s -> %s (source change; wire OK)", tag, o.Name, n.Name)})
		}
	}
	for tag, n := range newByTag {
		if _, ok := oldByTag[tag]; ok {
			continue
		}
		if n.Required {
			out = append(out, Change{tag, Breaking,
				fmt.Sprintf("required field %q added on tag %d (old messages lack it)", n.Name, tag)})
		} else {
			out = append(out, Change{tag, Safe,
				fmt.Sprintf("optional field %q added on tag %d", n.Name, tag)})
		}
	}

	sort.Slice(out, func(i, j int) bool {
		if out[i].Tag != out[j].Tag {
			return out[i].Tag < out[j].Tag
		}
		return out[i].Severity > out[j].Severity
	})
	return out
}

// HasBreaking reports whether any change is wire/semantically breaking —
// the gate you'd put in CI to block an incompatible contract change.
func HasBreaking(changes []Change) bool {
	for _, c := range changes {
		if c.Severity == Breaking {
			return true
		}
	}
	return false
}

func index(fs []Field) map[int]Field {
	m := make(map[int]Field, len(fs))
	for _, f := range fs {
		m[f.Tag] = f
	}
	return m
}
