package catalogreconcile

import (
	"bytes"
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	catalogassignmentidentityv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalogassignmentidentityv1"
	catalogpublicationv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalogpublicationv1"
	layoutactivationsetv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/layoutactivationsetv1"
	layoutbundlekeyv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/layoutbundlekeyv1"
	layoutroutingsecurityv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/layoutroutingsecurityv1"
	layoutvalidationviewv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/layoutvalidationviewv1"
	"github.com/pingcap-inc/nexuscrm-controlplane/catalogpublication"
)

const phaseOnePublicationProvenance = "nexuscrm.phase-one-no-layout-publication/v1"

// PhaseOnePublicationResult is the exact immutable lineage selected after a
// stopped Catalog reconcile. It is evidence, not a second serving selector.
type PhaseOnePublicationResult struct {
	PublicationDigest          string
	AssignmentViewDigest       string
	RoutingSecurityViewDigest  string
	LayoutValidationViewDigest string
	LayoutActivationSetDigest  string
}

type phaseOneArtifact struct {
	kind   string
	digest string
	raw    []byte
}

type preparedPhaseOnePublication struct {
	assignment catalogassignmentidentityv1.View
	binding    catalogpublicationv1.Binding
	artifacts  []phaseOneArtifact
	result     PhaseOnePublicationResult
}

// PublishPhaseOneConsumerLineage publishes the exact Catalog consumer lineage
// required by the phase-one Runtime API after Catalog reconcile has committed.
// It explicitly publishes an empty layout activation set: sfcrm continues to
// own its L1 UI semantics, and this function creates no layout authoring path.
// The caller must guarantee that the deployment is stopped and quiesced.
func PublishPhaseOneConsumerLineage(
	ctx context.Context,
	db *sql.DB,
	artifactStoreRoot string,
	appID string,
	runtimeAudience string,
	commandID string,
	output Output,
) (PhaseOnePublicationResult, error) {
	if db == nil {
		return PhaseOnePublicationResult{}, fmt.Errorf("phase-one Catalog publication requires a database")
	}
	prepared, err := preparePhaseOnePublication(appID, output)
	if err != nil {
		return PhaseOnePublicationResult{}, err
	}
	authority, err := catalogpublication.New(db)
	if err != nil {
		return PhaseOnePublicationResult{}, err
	}
	assignmentDigest, err := authority.StoreAssignmentIdentityView(ctx, prepared.assignment)
	if err != nil {
		return PhaseOnePublicationResult{}, err
	}
	if assignmentDigest != prepared.result.AssignmentViewDigest {
		return PhaseOnePublicationResult{}, fmt.Errorf("stored Catalog assignment identity digest changed")
	}
	for _, artifact := range prepared.artifacts {
		if err := putPhaseOneArtifact(artifactStoreRoot, artifact); err != nil {
			return PhaseOnePublicationResult{}, err
		}
	}

	outcome, err := authority.AdoptStoppedWriterState(ctx, catalogpublication.StoppedAdoptionRequest{
		CommandID:       commandID,
		RuntimeAudience: runtimeAudience,
		Candidate:       prepared.binding,
	})
	if err != nil {
		return PhaseOnePublicationResult{}, fmt.Errorf("adopt stopped Catalog publication: %w", err)
	}
	if outcome.Result != catalogpublication.ResultSelected &&
		outcome.Result != catalogpublication.ResultUnchanged {
		return PhaseOnePublicationResult{}, fmt.Errorf("stopped Catalog publication result is %s", outcome.Result)
	}
	if !outcome.ResultingActive.Present ||
		outcome.ResultingActive.Digest != prepared.result.PublicationDigest {
		return PhaseOnePublicationResult{}, fmt.Errorf("stopped Catalog publication selected a different binding")
	}
	return prepared.result, nil
}

func preparePhaseOnePublication(
	appID string,
	output Output,
) (preparedPhaseOnePublication, error) {
	snapshot := catalogpublicationv1.CatalogSnapshotBinding{
		OrgID:                 output.OrgID,
		CatalogRevision:       output.CatalogRevision,
		CatalogSnapshotDigest: output.CatalogSnapshotDigest,
		SchemaGeneration:      output.SchemaGeneration,
		EngineGeneration:      output.EngineGeneration,
	}
	if err := catalogpublicationv1.ValidateCatalogSnapshotBinding(snapshot); err != nil {
		return preparedPhaseOnePublication{}, fmt.Errorf("phase-one Catalog snapshot binding: %w", err)
	}
	assignment := catalogassignmentidentityv1.View{
		SourceCatalogSnapshot:         snapshot,
		CanonicalizationVersion:       catalogassignmentidentityv1.CanonicalizationVersion,
		KeyGrammarVersion:             catalogassignmentidentityv1.KeyGrammarVersion,
		ObjectKeyNormalizationVersion: catalogassignmentidentityv1.ObjectKeyNormalizationVersion,
		Objects:                       []catalogassignmentidentityv1.ObjectIdentity{},
		RecordTypes:                   []catalogassignmentidentityv1.RecordTypeIdentity{},
		ProductApps: []catalogassignmentidentityv1.ProductAppIdentity{{
			Key:   catalogassignmentidentityv1.ProductAppKey{AppID: appID},
			State: catalogassignmentidentityv1.IdentityStateActive,
			ResolvedTarget: catalogassignmentidentityv1.ProductAppResolvedTarget{
				Kind:                      catalogassignmentidentityv1.ProductAppResolvedTargetKindAppInstallationID,
				ResolvedAppInstallationID: output.AppInstallationID,
			},
		}},
		NavigationApps: []catalogassignmentidentityv1.NavigationAppIdentity{},
	}
	assignmentDigest, err := catalogassignmentidentityv1.ViewDigest(assignment)
	if err != nil {
		return preparedPhaseOnePublication{}, fmt.Errorf("identify Catalog assignment view: %w", err)
	}
	required := layoutbundlekeyv1.RequiredBundleKeySet{
		Version:    layoutbundlekeyv1.RequiredBundleKeySetVersion,
		BundleKeys: []layoutbundlekeyv1.BundleKey{},
	}
	requiredRaw, err := layoutbundlekeyv1.CanonicalRequiredBundleKeySetBytes(required)
	if err != nil {
		return preparedPhaseOnePublication{}, err
	}
	requiredDigest, err := layoutbundlekeyv1.RequiredBundleKeySetDigest(required)
	if err != nil {
		return preparedPhaseOnePublication{}, err
	}
	routing := layoutroutingsecurityv1.View{
		OrgID:                      output.OrgID,
		CanonicalizationVersion:    layoutroutingsecurityv1.ViewCanonicalizationVersion,
		KeyGrammarVersion:          layoutroutingsecurityv1.KeyGrammarVersion,
		RoleTopologyGrammarVersion: layoutroutingsecurityv1.RoleTopologyGrammarVersion,
		Profiles:                   []layoutroutingsecurityv1.ProfileIdentity{},
		Roles:                      []layoutroutingsecurityv1.RoleIdentity{},
	}
	routingRaw, err := layoutroutingsecurityv1.CanonicalViewBytes(routing)
	if err != nil {
		return preparedPhaseOnePublication{}, err
	}
	routingDigest, err := layoutroutingsecurityv1.ViewDigest(routing)
	if err != nil {
		return preparedPhaseOnePublication{}, err
	}
	validation := layoutvalidationviewv1.View{
		CanonicalizationVersion:             layoutvalidationviewv1.CanonicalizationVersion,
		KeyGrammarVersion:                   layoutvalidationviewv1.KeyGrammarVersion,
		ObjectKeyNormalizationVersion:       layoutvalidationviewv1.ObjectKeyNormalizationVersion,
		FieldKeyNormalizationVersion:        layoutvalidationviewv1.FieldKeyNormalizationVersion,
		SourceCatalogSnapshot:               snapshot,
		CatalogAssignmentIdentityViewDigest: assignmentDigest,
		RequiredBundleKeySetDigest:          requiredDigest,
		Objects:                             []layoutvalidationviewv1.ObjectContract{},
		Fields:                              []layoutvalidationviewv1.FieldContract{},
		Relationships:                       []layoutvalidationviewv1.RelationshipContract{},
		RelatedLists:                        []layoutvalidationviewv1.RelatedListContract{},
		Actions:                             []layoutvalidationviewv1.ActionContract{},
		RendererComponents:                  []layoutvalidationviewv1.RendererComponentContract{},
		AcceptedLayoutFormats:               []layoutvalidationviewv1.LayoutFormatContract{},
		BundleAuthorityEvidence:             []layoutvalidationviewv1.BundleAuthorityEvidenceRef{},
	}
	validationRaw, err := layoutvalidationviewv1.CanonicalViewBytes(validation)
	if err != nil {
		return preparedPhaseOnePublication{}, err
	}
	validationDigest, err := layoutvalidationviewv1.ViewDigest(validation)
	if err != nil {
		return preparedPhaseOnePublication{}, err
	}
	provenanceRaw := []byte(
		phaseOnePublicationProvenance + "\n" + assignmentDigest + "\n" +
			routingDigest + "\n" + validationDigest + "\n" + requiredDigest + "\n",
	)
	provenanceDigest := phaseOneDigest(provenanceRaw)
	activation := layoutactivationsetv1.Set{
		CanonicalizationVersion: layoutactivationsetv1.CanonicalizationVersion,
		OrgID:                   output.OrgID,
		CatalogSchema: catalogpublicationv1.CatalogSchemaBinding{
			CatalogSnapshot:            snapshot,
			LayoutValidationViewDigest: validationDigest,
		},
		RoutingSecurity: layoutroutingsecurityv1.Binding{
			OrgID:                           output.OrgID,
			SecurityEpoch:                   output.SecurityEpoch,
			LayoutRoutingSecurityViewDigest: routingDigest,
		},
		RequiredBundleKeySetDigest:  requiredDigest,
		PreparationProvenanceDigest: provenanceDigest,
		Entries:                     []layoutactivationsetv1.Entry{},
	}
	activationRaw, err := layoutactivationsetv1.CanonicalSetBytes(activation)
	if err != nil {
		return preparedPhaseOnePublication{}, err
	}
	activationDigest, err := layoutactivationsetv1.SetDigest(activation)
	if err != nil {
		return preparedPhaseOnePublication{}, err
	}
	binding := catalogpublicationv1.Binding{
		OrgID:                           output.OrgID,
		CatalogRevision:                 output.CatalogRevision,
		CatalogSnapshotDigest:           output.CatalogSnapshotDigest,
		SchemaGeneration:                output.SchemaGeneration,
		EngineGeneration:                output.EngineGeneration,
		LayoutValidationViewDigest:      validationDigest,
		LayoutActivationSetDigest:       activationDigest,
		SecurityEpoch:                   output.SecurityEpoch,
		LayoutRoutingSecurityViewDigest: routingDigest,
	}
	publicationDigest, err := catalogpublicationv1.BindingDigest(binding)
	if err != nil {
		return preparedPhaseOnePublication{}, err
	}
	return preparedPhaseOnePublication{
		assignment: assignment,
		binding:    binding,
		artifacts: []phaseOneArtifact{
			{kind: "required-layout-bundle-key-set-v1", digest: requiredDigest, raw: requiredRaw},
			{kind: "layout-routing-security-view-v1", digest: routingDigest, raw: routingRaw},
			{kind: "layout-validation-view-v1", digest: validationDigest, raw: validationRaw},
			{kind: "layout-preparation-provenance-v1", digest: provenanceDigest, raw: provenanceRaw},
			{kind: "layout-activation-set-v1", digest: activationDigest, raw: activationRaw},
		},
		result: PhaseOnePublicationResult{
			PublicationDigest:          publicationDigest,
			AssignmentViewDigest:       assignmentDigest,
			RoutingSecurityViewDigest:  routingDigest,
			LayoutValidationViewDigest: validationDigest,
			LayoutActivationSetDigest:  activationDigest,
		},
	}, nil
}

func putPhaseOneArtifact(root string, artifact phaseOneArtifact) error {
	if root == "" {
		return fmt.Errorf("phase-one Catalog artifact store root is required")
	}
	if len(artifact.digest) != 71 || !strings.HasPrefix(artifact.digest, "sha256:") {
		return fmt.Errorf("phase-one Catalog artifact digest is invalid")
	}
	directory := filepath.Join(root, artifact.kind)
	if err := os.MkdirAll(directory, 0o750); err != nil {
		return fmt.Errorf("create phase-one Catalog artifact directory: %w", err)
	}
	path := filepath.Join(directory, artifact.digest[len("sha256:"):]+".bin")
	if existing, err := os.ReadFile(path); err == nil {
		if !bytes.Equal(existing, artifact.raw) {
			return fmt.Errorf("phase-one Catalog artifact digest names different bytes")
		}
		return nil
	} else if !errors.Is(err, os.ErrNotExist) {
		return err
	}
	temp, err := os.CreateTemp(directory, ".phase-one-catalog-artifact-*")
	if err != nil {
		return err
	}
	tempName := temp.Name()
	defer os.Remove(tempName)
	if _, err := temp.Write(artifact.raw); err != nil {
		_ = temp.Close()
		return err
	}
	if err := temp.Sync(); err != nil {
		_ = temp.Close()
		return err
	}
	if err := temp.Chmod(0o440); err != nil {
		_ = temp.Close()
		return err
	}
	if err := temp.Close(); err != nil {
		return err
	}
	if err := os.Link(tempName, path); err != nil {
		if !errors.Is(err, os.ErrExist) {
			return err
		}
		existing, readErr := os.ReadFile(path)
		if readErr != nil {
			return fmt.Errorf("read concurrent phase-one Catalog artifact: %w", readErr)
		}
		if !bytes.Equal(existing, artifact.raw) {
			return fmt.Errorf("concurrent phase-one Catalog artifact differs")
		}
	}
	dir, err := os.Open(directory)
	if err != nil {
		return err
	}
	defer dir.Close()
	return dir.Sync()
}

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