package source

import (
	"fmt"
	"strings"
)

// DocumentIndex is an immutable snapshot of a source document's identity and
// unit positions for repeated source-reference operations.
type DocumentIndex struct {
	documentID  string
	positions   map[int]int
	hasDocument bool
}

// NewDocumentIndex snapshots doc without retaining or mutating it.
func NewDocumentIndex(doc *SourceDocument) DocumentIndex {
	if doc == nil {
		return DocumentIndex{}
	}
	positions := make(map[int]int, len(doc.Units))
	for position, unit := range doc.Units {
		if _, exists := positions[unit.ID]; !exists {
			positions[unit.ID] = position
		}
	}
	return DocumentIndex{
		documentID:  doc.ID,
		positions:   positions,
		hasDocument: true,
	}
}

// DocumentID returns the indexed document identity.
func (i DocumentIndex) DocumentID() (string, bool) {
	if !i.hasDocument {
		return "", false
	}
	return i.documentID, true
}

// Position returns the indexed document position for unitID.
func (i DocumentIndex) Position(unitID int) (int, bool) {
	position, ok := i.positions[unitID]
	return position, ok
}

// ValidateRef validates ref against the indexed document snapshot.
func (i DocumentIndex) ValidateRef(ref SourceRef) error {
	if !i.hasDocument {
		return fmt.Errorf("source document must not be nil")
	}
	return validateRef(i.documentID, i.Position, ref)
}

func ValidateDocument(doc *SourceDocument) error {
	if doc == nil {
		return fmt.Errorf("source document must not be nil")
	}
	if isBlank(doc.ID) {
		return fmt.Errorf("source document id must not be empty")
	}
	if hasSurroundingWhitespace(doc.ID) {
		return fmt.Errorf("source document id %q must not contain leading or trailing whitespace", doc.ID)
	}
	if isBlank(doc.Kind) {
		return fmt.Errorf("source document kind must not be empty")
	}
	if isBlank(doc.Format) {
		return fmt.Errorf("source document format must not be empty")
	}
	if isBlank(doc.Digest) {
		return fmt.Errorf("source document digest must not be empty")
	}
	if len(doc.Units) == 0 {
		return fmt.Errorf("source document units must not be empty")
	}

	seenUnitIDs := make(map[int]struct{}, len(doc.Units))
	for i, unit := range doc.Units {
		if unit.ID <= 0 {
			return fmt.Errorf("source unit[%d].id must be positive", i)
		}
		if isBlank(unit.Kind) {
			return fmt.Errorf("source unit[%d].kind must not be empty", i)
		}
		if isBlank(unit.Text) {
			return fmt.Errorf("source unit[%d].text must not be empty", i)
		}
		if _, ok := seenUnitIDs[unit.ID]; ok {
			return fmt.Errorf("source unit id %d is duplicated", unit.ID)
		}
		seenUnitIDs[unit.ID] = struct{}{}
	}
	for i, unit := range doc.Units {
		if err := ValidateRef(doc, unit.Ref); err != nil {
			return fmt.Errorf("source unit[%d].ref: %w", i, err)
		}
		if unit.Ref.StartUnitID != unit.ID || unit.Ref.EndUnitID != unit.ID {
			return fmt.Errorf("source unit[%d].ref must identify source unit id %d", i, unit.ID)
		}
	}

	return nil
}

func ValidateRef(doc *SourceDocument, ref SourceRef) error {
	if doc == nil {
		return fmt.Errorf("source document must not be nil")
	}
	return validateRef(doc.ID, func(unitID int) (int, bool) {
		return UnitIndex(doc, unitID)
	}, ref)
}

func validateRef(documentID string, position func(int) (int, bool), ref SourceRef) error {
	if isBlank(ref.SourceID) {
		return fmt.Errorf("source ref source_id must not be empty")
	}
	if hasSurroundingWhitespace(ref.SourceID) {
		return fmt.Errorf("source ref source_id %q must not contain leading or trailing whitespace", ref.SourceID)
	}
	if ref.StartUnitID <= 0 {
		return fmt.Errorf("source ref start_unit_id must be positive")
	}
	if ref.EndUnitID <= 0 {
		return fmt.Errorf("source ref end_unit_id must be positive")
	}
	if ref.SourceID != documentID {
		return fmt.Errorf("source ref source_id %q does not match document id %q", ref.SourceID, documentID)
	}

	startIndex, ok := position(ref.StartUnitID)
	if !ok {
		return fmt.Errorf("source ref start_unit_id %d was not found", ref.StartUnitID)
	}
	endIndex, ok := position(ref.EndUnitID)
	if !ok {
		return fmt.Errorf("source ref end_unit_id %d was not found", ref.EndUnitID)
	}
	if startIndex > endIndex {
		return fmt.Errorf("source ref start_unit_id %d appears after end_unit_id %d", ref.StartUnitID, ref.EndUnitID)
	}

	return nil
}

func UnitIndex(doc *SourceDocument, unitID int) (int, bool) {
	if doc == nil {
		return 0, false
	}
	for i, unit := range doc.Units {
		if unit.ID == unitID {
			return i, true
		}
	}
	return 0, false
}

func isBlank(value string) bool {
	return strings.TrimSpace(value) == ""
}

func hasSurroundingWhitespace(value string) bool {
	return strings.TrimSpace(value) != value
}
