package transcript

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"math"
	"math/big"
	"strconv"
	"strings"

	"gitea.maximumdirect.net/eric/notarius/internal/core/source"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)

const Key = "seriatim"

const (
	DocumentKind = "transcript"
	UnitKind     = "transcript_segment"
	Format       = "application/vnd.seriatim+json"
)

var providedCapabilities = []string{
	"source.transcript",
	"transcript.speaker",
	"transcript.timestamps",
}

var _ contracts.InputAdapter = (*Adapter)(nil)

type Options struct{}

type Adapter struct {
	options Options
}

func New() *Adapter {
	options, _ := DecodeOptions(nil)
	return &Adapter{options: options}
}

func (a *Adapter) Key() string {
	return Key
}

func (a *Adapter) Parse(ctx context.Context, req contracts.ParseRequest) (*source.SourceDocument, error) {
	if ctx == nil {
		return nil, inputErrorf("context must not be nil")
	}
	if err := ctx.Err(); err != nil {
		return nil, inputErrorf("context error before parsing: %w", err)
	}
	if len(req.Raw) == 0 {
		return nil, inputErrorf("raw input must not be empty")
	}

	parsed, err := decodeTranscript(req.Raw)
	if err != nil {
		return nil, inputErrorf("parse JSON: %w", err)
	}
	if len(parsed.Segments) == 0 {
		return nil, inputErrorf("segments must not be empty")
	}

	rawDigest := digest(req.Raw)
	doc := &source.SourceDocument{
		ID:       documentID(req.SourceID, parsed.Metadata, rawDigest),
		Kind:     DocumentKind,
		Format:   Format,
		Metadata: copyMetadata(parsed.Metadata),
	}

	seenSegmentIDs := make(map[int]struct{}, len(parsed.Segments))
	for i, segment := range parsed.Segments {
		unit, err := sourceUnit(doc.ID, segment, i, seenSegmentIDs)
		if err != nil {
			return nil, err
		}
		doc.Units = append(doc.Units, unit)
	}
	doc.Digest, err = source.DigestDocument(doc)
	if err != nil {
		return nil, inputErrorf("digest source document: %w", err)
	}

	if err := source.ValidateDocument(doc); err != nil {
		return nil, inputErrorf("validate source document: %w", err)
	}
	return doc, nil
}

func ModuleSpec() pipeline.ModuleSpec {
	return pipeline.ModuleSpec{
		Key:            Key,
		Stage:          pipeline.StageInput,
		ExecutionClass: contracts.ExecutionClassDeterministic,
		Provides:       append([]string(nil), providedCapabilities...),
	}
}

func Register(registry *pipeline.InputAdapterRegistry) error {
	return registry.RegisterBuilderWithSpec(ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.InputAdapter, error) {
		options, err := DecodeOptions(request.Options)
		if err != nil {
			return nil, err
		}
		return &Adapter{options: options}, nil
	})
}

func validateOptions(options map[string]any) error {
	_, err := DecodeOptions(options)
	return err
}

func DecodeOptions(options map[string]any) (Options, error) {
	if err := pipeline.RejectUnknownOptions(options); err != nil {
		return Options{}, inputErrorf("%w", err)
	}
	return Options{}, nil
}

func sourceUnit(sourceID string, segment segment, index int, seen map[int]struct{}) (source.SourceUnit, error) {
	segmentLabel := fmt.Sprintf("segment[%d]", index)
	if segment.ID <= 0 {
		return source.SourceUnit{}, inputErrorf("%s id must be positive", segmentLabel)
	}
	if _, ok := seen[segment.ID]; ok {
		return source.SourceUnit{}, inputErrorf("segment id %d is duplicated", segment.ID)
	}
	seen[segment.ID] = struct{}{}

	speaker := strings.TrimSpace(segment.Speaker)
	if speaker == "" {
		return source.SourceUnit{}, inputErrorf("segment %d speaker must not be empty", segment.ID)
	}

	start, err := validTimestamp(segment.Start, fmt.Sprintf("segment %d start", segment.ID))
	if err != nil {
		return source.SourceUnit{}, err
	}
	end, err := validTimestamp(segment.End, fmt.Sprintf("segment %d end", segment.ID))
	if err != nil {
		return source.SourceUnit{}, err
	}
	if end.Cmp(start) < 0 {
		return source.SourceUnit{}, inputErrorf("segment %d end must be greater than or equal to start", segment.ID)
	}

	if strings.TrimSpace(segment.Text) == "" {
		return source.SourceUnit{}, inputErrorf("segment %d text must not be empty", segment.ID)
	}

	return source.SourceUnit{
		ID:   segment.ID,
		Kind: UnitKind,
		Text: segment.Text,
		Ref: source.SourceRef{
			SourceID:    sourceID,
			StartUnitID: segment.ID,
			EndUnitID:   segment.ID,
		},
		Metadata: map[string]any{
			MetadataSpeaker: segment.Speaker,
			MetadataStart:   segment.Start,
			MetadataEnd:     segment.End,
		},
	}, nil
}

func validTimestamp(value fmt.Stringer, label string) (*big.Rat, error) {
	raw := strings.TrimSpace(value.String())
	if raw == "" {
		return nil, inputErrorf("%s must not be empty", label)
	}
	parsed, err := strconv.ParseFloat(raw, 64)
	if err != nil {
		return nil, inputErrorf("%s must be a valid number: %w", label, err)
	}
	if math.IsInf(parsed, 0) || math.IsNaN(parsed) {
		return nil, inputErrorf("%s must be finite", label)
	}
	if parsed < 0 {
		return nil, inputErrorf("%s must not be negative", label)
	}
	rat, ok := new(big.Rat).SetString(raw)
	if !ok {
		return nil, inputErrorf("%s must be a valid number", label)
	}
	return rat, nil
}

func documentID(requestedID string, metadata map[string]any, rawDigest string) string {
	if id := strings.TrimSpace(requestedID); id != "" {
		return id
	}
	if id := stringMetadata(metadata, "id"); id != "" {
		return id
	}
	if id := stringMetadata(metadata, "source_id"); id != "" {
		return id
	}
	return "seriatim:" + strings.TrimPrefix(rawDigest, "sha256:")[:16]
}

func stringMetadata(metadata map[string]any, key string) string {
	value, ok := metadata[key].(string)
	if !ok {
		return ""
	}
	return strings.TrimSpace(value)
}

func copyMetadata(metadata map[string]any) map[string]any {
	if len(metadata) == 0 {
		return nil
	}
	copied := make(map[string]any, len(metadata))
	for key, value := range metadata {
		copied[key] = value
	}
	return copied
}

func digest(raw []byte) string {
	sum := sha256.Sum256(raw)
	return "sha256:" + hex.EncodeToString(sum[:])
}

func inputErrorf(format string, args ...any) error {
	return fmt.Errorf("seriatim input: "+format, args...)
}
