// Package applicationnavigation owns seed-once installation and explicit
// migration of the current navigation aggregate. It never publishes Catalog.
package applicationnavigation

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"

	database "github.com/nexuscrm/db"
	"github.com/nexuscrm/persistence"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/applicationnavigationv1"
)

const Schema = "nexuscrm.application-navigation-initialization/v1"

type Migration struct {
	SourceDigest string          `json:"source_digest"`
	Items        []semantic.Item `json:"items"`
}
type Input struct {
	Schema        string          `json:"schema"`
	OwnerID       string          `json:"owner_id"`
	ApplicationID string          `json:"application_id"`
	Template      []semantic.Item `json:"template"`
	Migration     *Migration      `json:"migration,omitempty"`
}
type Result struct {
	Schema       string            `json:"schema"`
	Disposition  string            `json:"disposition"`
	SourceDigest string            `json:"source_digest"`
	Document     semantic.Document `json:"document"`
}

func DecodeInput(reader io.Reader) (Input, error) {
	var input Input
	decoder := json.NewDecoder(io.LimitReader(reader, semantic.MaxSemanticRequestBytes+1))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&input); err != nil {
		return input, err
	}
	if err := decoder.Decode(new(any)); err != io.EOF {
		return input, errors.New("unexpected trailing initialization data")
	}
	return input, ValidateInput(input)
}
func ValidateInput(input Input) error {
	if input.Schema != Schema || !semantic.ValidIdentifier(input.OwnerID) || !semantic.ValidIdentifier(input.ApplicationID) {
		return errors.New("invalid navigation initialization identity")
	}
	if err := semantic.ValidateItems(input.Template); err != nil {
		return err
	}
	if input.Migration != nil {
		if len(input.Migration.SourceDigest) != 64 {
			return errors.New("migration requires the observed source digest")
		}
		if err := semantic.ValidateItems(input.Migration.Items); err != nil {
			return err
		}
	}
	return nil
}

// Apply preserves every initialized document, including an empty list. An
// existing revision-zero document is never replaced by a release template.
// Legacy dialect translation is supplied explicitly by the L1/L2 installer.
func Apply(ctx context.Context, connection *database.TiDBConnection, input Input, assertOnly bool) (Result, error) {
	if err := ValidateInput(input); err != nil {
		return Result{}, err
	}
	if connection == nil || connection.DB() == nil {
		return Result{}, errors.New("navigation initialization requires a database")
	}
	if err := persistence.VerifyApplicationNavigationSchema(ctx, connection.DB()); err != nil {
		return Result{}, err
	}
	manager := persistence.NewTransactionManager(connection)
	repository := persistence.NewApplicationNavigationRepository(connection.DB(), manager)
	var result Result
	err := manager.WithPessimisticTransaction(ctx, func(txCtx context.Context) error {
		source, err := repository.LockInitializationSource(txCtx, input.OwnerID, input.ApplicationID)
		if err != nil {
			return err
		}
		result = Result{Schema: Schema, Disposition: "preserved", SourceDigest: source.Digest}
		if source.Revision == 0 {
			if assertOnly {
				return errors.New("navigation is not initialized")
			}
			items := input.Template
			raw := bytes.TrimSpace(source.RawItems)
			if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
				result.Disposition = "initialized"
			} else {
				result.Disposition = "migrated"
				// A canonical document at revision zero (including []) is a
				// valid current value. Preserve it without guessing customization.
				decoder := json.NewDecoder(bytes.NewReader(raw))
				decoder.DisallowUnknownFields()
				if decoder.Decode(&items) != nil || semantic.ValidateItems(items) != nil {
					if input.Migration == nil {
						return fmt.Errorf("application %q needs an explicit legacy navigation migration (source %s)", input.ApplicationID, source.Digest)
					}
					if input.Migration.SourceDigest != source.Digest {
						return persistence.ErrNavigationConflict
					}
					items = input.Migration.Items
				}
			}
			if _, err := repository.Initialize(txCtx, input.OwnerID, input.ApplicationID, items, source.Digest); err != nil {
				return err
			}
		}
		result.Document, err = repository.Read(txCtx, input.OwnerID, input.ApplicationID)
		return err
	})
	return result, err
}
