package evidencecontext

import (
	"bytes"
	"embed"
	"encoding/json"
	"fmt"
	"io"
	"strings"
	"sync"

	"gitea.maximumdirect.net/eric/notarius/internal/core/source"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
	"github.com/santhosh-tekuri/jsonschema/v6"
)

//go:embed assets/schemas/source_evidence_context.v1.json
var schemaAssets embed.FS

var (
	loadSchemaOnce sync.Once
	loadedSchema   []byte
	compiledSchema *jsonschema.Schema
	loadSchemaErr  error
)

// Codec owns strict serialization for the durable evidence-context contract.
type Codec struct{}

func New() *Codec { return &Codec{} }

func (c *Codec) Kind() contracts.ArtifactKind { return ArtifactKind }

func (c *Codec) Schema() contracts.ArtifactSchema {
	raw, err := c.schemaBytes()
	if err != nil {
		return contracts.ArtifactSchema{}
	}
	return contracts.ArtifactSchema{ID: SchemaID, Name: SchemaName, Version: SchemaVersion, JSONSchema: raw}
}

func (c *Codec) MediaType() string { return MediaType }

// Serialize builds and encodes the framework-owned serialized artifact.
func Serialize(request BuildRequest) (contracts.SerializedArtifact, error) {
	value, err := Build(request)
	if err != nil {
		return contracts.SerializedArtifact{}, err
	}
	codec := New()
	content, err := codec.Encode(value)
	if err != nil {
		return contracts.SerializedArtifact{}, err
	}
	return contracts.SerializedArtifact{Kind: ArtifactKind, Schema: codec.Schema(), MediaType: MediaType, Content: content}, nil
}

func (c *Codec) Encode(value Document) ([]byte, error) {
	if _, err := c.schemaBytes(); err != nil {
		return nil, err
	}
	canonical, err := canonicalize(value)
	if err != nil {
		return nil, fmt.Errorf("encode evidence context: %w", err)
	}
	content, err := json.Marshal(canonical)
	if err != nil {
		return nil, fmt.Errorf("encode evidence context: %w", err)
	}
	if err := validateSchemaInstance(content); err != nil {
		return nil, fmt.Errorf("encode evidence context: %w", err)
	}
	return content, nil
}

func (c *Codec) Decode(content []byte) (Document, error) {
	if _, err := c.schemaBytes(); err != nil {
		return nil, err
	}
	if err := validateSchemaInstance(content); err != nil {
		return nil, fmt.Errorf("decode evidence context: %w", err)
	}
	decoder := json.NewDecoder(bytes.NewReader(content))
	decoder.DisallowUnknownFields()
	var value Document
	if err := decoder.Decode(&value); err != nil {
		return nil, fmt.Errorf("decode evidence context: %w", err)
	}
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		return nil, fmt.Errorf("decode evidence context: multiple JSON values")
	}
	canonical, err := canonicalizeOwned(value)
	if err != nil {
		return nil, fmt.Errorf("decode evidence context: %w", err)
	}
	return canonical, nil
}

func (c *Codec) schemaBytes() ([]byte, error) {
	loadSchemaOnce.Do(loadAndCompileSchema)
	if loadSchemaErr != nil {
		return nil, loadSchemaErr
	}
	return append([]byte(nil), loadedSchema...), nil
}

func loadAndCompileSchema() {
	raw, err := schemaAssets.ReadFile("assets/schemas/source_evidence_context.v1.json")
	if err != nil {
		loadSchemaErr = fmt.Errorf("read source evidence context schema: %w", err)
		return
	}
	var identity struct {
		ID    string `json:"$id"`
		Title string `json:"title"`
		Type  string `json:"type"`
	}
	if err := json.Unmarshal(raw, &identity); err != nil {
		loadSchemaErr = fmt.Errorf("decode source evidence context schema: %w", err)
		return
	}
	if identity.ID != SchemaID || identity.Title != SchemaName || identity.Type != "array" {
		loadSchemaErr = fmt.Errorf("source evidence context schema identity is invalid")
		return
	}
	schemaDocument, err := jsonschema.UnmarshalJSON(bytes.NewReader(raw))
	if err != nil {
		loadSchemaErr = fmt.Errorf("parse source evidence context schema: %w", err)
		return
	}
	compiler := jsonschema.NewCompiler()
	if err := compiler.AddResource("source-evidence-context-schema.json", schemaDocument); err != nil {
		loadSchemaErr = fmt.Errorf("load source evidence context schema: %w", err)
		return
	}
	compiled, err := compiler.Compile("source-evidence-context-schema.json")
	if err != nil {
		loadSchemaErr = fmt.Errorf("compile source evidence context schema: %w", err)
		return
	}
	loadedSchema = append([]byte(nil), raw...)
	compiledSchema = compiled
}

func validateSchemaInstance(content []byte) error {
	instance, err := jsonschema.UnmarshalJSON(bytes.NewReader(content))
	if err != nil {
		return fmt.Errorf("payload is not valid JSON: %w", err)
	}
	if err := compiledSchema.Validate(instance); err != nil {
		return fmt.Errorf("payload does not conform to source evidence context schema: %w", err)
	}
	return nil
}

func canonicalize(value Document) (Document, error) {
	owned, err := clone(value)
	if err != nil {
		return nil, err
	}
	return canonicalizeOwned(owned)
}

func canonicalizeOwned(value Document) (Document, error) {
	if value == nil {
		return nil, fmt.Errorf("document must be a JSON array")
	}
	seenUnitIDs := make(map[int]struct{}, len(value))
	sourceID := ""
	for unitIndex := range value {
		unit := value[unitIndex]
		if unit.ID <= 0 || strings.TrimSpace(unit.Kind) == "" || strings.TrimSpace(unit.Text) == "" {
			return nil, fmt.Errorf("units[%d] has invalid required fields", unitIndex)
		}
		if err := validateUnitRef(unit, unitIndex); err != nil {
			return nil, err
		}
		if sourceID == "" {
			sourceID = unit.Ref.SourceID
		} else if unit.Ref.SourceID != sourceID {
			return nil, fmt.Errorf("units[%d].ref.source_id must match units[0].ref.source_id", unitIndex)
		}
		if _, exists := seenUnitIDs[unit.ID]; exists {
			return nil, fmt.Errorf("units contains duplicate unit id %d", unit.ID)
		}
		seenUnitIDs[unit.ID] = struct{}{}
	}
	return value, nil
}

func validateUnitRef(unit source.SourceUnit, unitIndex int) error {
	prefix := fmt.Sprintf("units[%d].ref", unitIndex)
	if strings.TrimSpace(unit.Ref.SourceID) == "" || strings.TrimSpace(unit.Ref.SourceID) != unit.Ref.SourceID {
		return fmt.Errorf("%s.source_id must be a non-empty trimmed string", prefix)
	}
	if unit.Ref.StartUnitID != unit.ID || unit.Ref.EndUnitID != unit.ID {
		return fmt.Errorf("%s must identify unit id %d", prefix, unit.ID)
	}
	return nil
}

func clone(value Document) (Document, error) {
	if value == nil {
		return nil, nil
	}
	cloned := make(Document, len(value))
	for unitIndex, unit := range value {
		owned, err := cloneSourceUnit(unit)
		if err != nil {
			return nil, fmt.Errorf("clone units[%d]: %w", unitIndex, err)
		}
		cloned[unitIndex] = owned
	}
	return cloned, nil
}

func cloneSourceUnit(unit source.SourceUnit) (source.SourceUnit, error) {
	metadata, err := source.CloneMetadata(unit.Metadata)
	if err != nil {
		return source.SourceUnit{}, fmt.Errorf("clone metadata: %w", err)
	}
	unit.Metadata = metadata
	return unit, nil
}
