package main

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"github.com/pingcap-inc/nexuscrm-controlplane/applicationnavigation"
	"os"
	"os/signal"
	"path/filepath"
	"syscall"
	"time"

	"github.com/nexuscrm/db"
	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/nexuscrm/persistence"
	"github.com/pingcap-inc/nexuscrm-controlplane/bootstrapimpl"
	"github.com/pingcap-inc/nexuscrm-controlplane/catalogapp"
	"github.com/pingcap-inc/nexuscrm-controlplane/catalogreconcile"
	"github.com/pingcap-inc/nexuscrm-controlplane/catalogtenant"
	"github.com/pingcap-inc/nexuscrm-controlplane/internal/catalogsystem"
	"github.com/pingcap-inc/nexuscrm-controlplane/packs"
	"github.com/pingcap-inc/nexuscrm-controlplane/runtimework"
)

func main() {
	if err := run(os.Args[1:]); err != nil {
		_, _ = fmt.Fprintf(os.Stderr, "catalog-reconcile: %v\n", err)
		os.Exit(1)
	}
}

func run(args []string) error {
	if len(args) > 0 && args[0] == "application-navigation" {
		return runApplicationNavigation(args[1:])
	}
	if len(args) > 0 && args[0] == "abandon-unpublished-upgrade" {
		return runAbandonUnpublishedUpgrade(args[1:])
	}
	if len(args) > 0 && args[0] == "work-ownership" {
		return runWorkOwnership(args[1:])
	}
	if len(args) > 0 && args[0] == "publisher-trust" {
		return runPublisherTrust(args[1:])
	}
	return runCatalogReconcile(args)
}

func runAbandonUnpublishedUpgrade(args []string) error {
	flags := flag.NewFlagSet(
		"catalog-reconcile abandon-unpublished-upgrade",
		flag.ContinueOnError,
	)
	operationID := flags.String("operation-id", "", "Catalog upgrade operation identity")
	evidenceDigest := flags.String("evidence-digest", "", "immutable abandonment evidence digest")
	outputPath := flags.String("output", "", "path for the canonical abandonment output")
	if err := flags.Parse(args); err != nil {
		return err
	}
	if flags.NArg() != 0 || *operationID == "" || *evidenceDigest == "" || *outputPath == "" {
		return errors.New(
			"abandon-unpublished-upgrade requires -operation-id, -evidence-digest, and -output and accepts no positional arguments",
		)
	}
	ctx, stop := signal.NotifyContext(
		context.Background(), syscall.SIGINT, syscall.SIGTERM,
	)
	defer stop()
	connection, err := database.NewConnection()
	if err != nil {
		return fmt.Errorf("connect TiDB: %w", err)
	}
	defer connection.Close()
	authority, err := catalogapp.New(connection.DB(), catalogapp.Options{})
	if err != nil {
		return err
	}
	if err := authority.EnsureSchema(ctx); err != nil {
		return fmt.Errorf("ensure Catalog schema: %w", err)
	}
	operation, err := authority.AbandonUnpublishedUpgrade(
		ctx, *operationID, *evidenceDigest,
	)
	if err != nil {
		return err
	}
	outputRaw, err := json.Marshal(struct {
		Schema         string `json:"schema"`
		OperationID    string `json:"operation_id"`
		State          string `json:"state"`
		Step           string `json:"step"`
		EvidenceDigest string `json:"evidence_digest"`
	}{
		Schema:         "nexuscrm.catalog-operation-abandonment/v1",
		OperationID:    operation.OperationID,
		State:          string(operation.State),
		Step:           operation.Step,
		EvidenceDigest: operation.LastErrorDigest,
	})
	if err != nil {
		return err
	}
	outputDigest := canonicalOutputDigest(outputRaw)
	if err := writeAtomic(*outputPath, append(outputRaw, '\n')); err != nil {
		return err
	}
	_, _ = fmt.Fprintf(os.Stdout, "%s\n", outputDigest)
	return nil
}

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

func runPublisherTrust(args []string) error {
	flags := flag.NewFlagSet("catalog-reconcile publisher-trust", flag.ContinueOnError)
	configPath := flags.String("config", "", "path to the immutable publisher trust transition config")
	outputPath := flags.String("output", "", "path for the canonical transition output")
	if err := flags.Parse(args); err != nil {
		return err
	}
	if flags.NArg() != 0 || *configPath == "" || *outputPath == "" {
		return errors.New("publisher-trust requires -config and -output and accepts no positional arguments")
	}
	rawConfig, err := os.ReadFile(*configPath)
	if err != nil {
		return fmt.Errorf("read publisher trust transition config: %w", err)
	}
	config, err := catalogreconcile.ParsePublisherTrustTransitionConfig(rawConfig)
	if err != nil {
		return err
	}
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()
	connection, err := database.NewConnection()
	if err != nil {
		return fmt.Errorf("connect TiDB: %w", err)
	}
	defer connection.Close()
	authority, err := catalogapp.New(connection.DB(), catalogapp.Options{})
	if err != nil {
		return err
	}
	if err := authority.EnsureSchema(ctx); err != nil {
		return fmt.Errorf("ensure Catalog schema: %w", err)
	}
	output, err := catalogreconcile.ApplyPublisherTrustTransition(
		ctx, authority.ReadPublisher, authority.TransitionPublisherTrust, config,
	)
	if err != nil {
		return err
	}
	outputRaw, outputDigest, err := catalogreconcile.MarshalPublisherTrustTransitionOutput(output)
	if err != nil {
		return err
	}
	if err := writeAtomic(*outputPath, append(outputRaw, '\n')); err != nil {
		return err
	}
	_, _ = fmt.Fprintf(os.Stdout, "%s\n", outputDigest)
	return nil
}

func runCatalogReconcile(args []string) error {
	flags := flag.NewFlagSet("catalog-reconcile", flag.ContinueOnError)
	configPath := flags.String("config", "", "path to the immutable Catalog reconcile config")
	outputPath := flags.String("output", "", "path for the canonical reconcile output")
	if err := flags.Parse(args); err != nil {
		return err
	}
	if flags.NArg() != 0 {
		return errors.New("Catalog reconcile does not accept positional arguments")
	}
	if *configPath == "" || *outputPath == "" {
		return errors.New("both -config and -output are required")
	}
	rawConfig, err := os.ReadFile(*configPath)
	if err != nil {
		return fmt.Errorf("read config: %w", err)
	}
	config, err := catalogreconcile.ParseConfig(rawConfig)
	if err != nil {
		return err
	}
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()
	connection, err := database.NewConnection()
	if err != nil {
		return fmt.Errorf("connect TiDB: %w", err)
	}
	defer connection.Close()
	authority, err := catalogapp.New(connection.DB(), catalogapp.Options{})
	if err != nil {
		return err
	}
	if err := authority.EnsureSchema(ctx); err != nil {
		return fmt.Errorf("ensure Catalog schema: %w", err)
	}
	tenantAuthority, err := catalogtenant.New(connection.DB())
	if err != nil {
		return err
	}
	if err := tenantAuthority.EnsureSchema(ctx); err != nil {
		return fmt.Errorf("ensure tenant Catalog authority schema: %w", err)
	}
	systemAuthority, err := catalogsystem.New(connection.DB())
	if err != nil {
		return err
	}
	if err := systemAuthority.EnsureSchema(ctx); err != nil {
		return fmt.Errorf("ensure system Catalog authority schema: %w", err)
	}
	var provisionRelease catalogreconcile.MissingReleaseProvisioner
	if config.ReleaseImport != nil {
		provisionRelease = func(ctx context.Context) (catalogapp.RegisteredRelease, error) {
			packageRaw, err := os.ReadFile(config.ReleaseImport.PackagePath)
			if err != nil {
				return catalogapp.RegisteredRelease{}, fmt.Errorf("read AppRelease package: %w", err)
			}
			attestationRaw, err := os.ReadFile(config.ReleaseImport.AttestationPath)
			if err != nil {
				return catalogapp.RegisteredRelease{}, fmt.Errorf("read AppRelease attestation: %w", err)
			}
			trust, err := config.ReleaseImport.PublisherTrust()
			if err != nil {
				return catalogapp.RegisteredRelease{}, err
			}
			return catalogreconcile.ProvisionRelease(ctx, authority, catalogreconcile.ReleaseInput{
				Publisher: trust, AppID: config.AppID, ExpectedAppReleaseDigest: config.ExpectedAppReleaseDigest,
				PackageBytes: packageRaw, Attestation: attestationRaw, Compatibility: config.Compatibility(),
			})
		}
	}
	release, err := catalogreconcile.ResolveRelease(ctx, authority.ReadRelease, config.AppID, config.ExpectedAppReleaseDigest, config.Compatibility(), provisionRelease)
	if err != nil {
		return err
	}
	reconcileRequest, err := config.BuildRequest(release)
	if err != nil {
		return err
	}
	if config.ApprovalPublication != nil {
		publicationRaw, err := os.ReadFile(config.ApprovalPublication.BundlePath)
		if err != nil {
			return fmt.Errorf("read Approval publication bundle: %w", err)
		}
		reconcileRequest, err = config.BindApprovalPublication(reconcileRequest, publicationRaw)
		if err != nil {
			return err
		}
	}
	store, err := catalogsnapshot.NewDirectoryStore(config.ArtifactStoreRoot)
	if err != nil {
		return err
	}
	metadataRepository := persistence.NewMetadataRepository(connection.DB())
	metadataReader, err := catalogreconcile.NewRepositoryMetadataReader(metadataRepository)
	if err != nil {
		return err
	}
	transactionManager := persistence.NewTransactionManager(connection)
	securityRepository := persistence.NewRuntimeSecurityRepository(connection.DB())
	metadataTransaction := withCatalogRuntimeMetadataPreparation(
		func(ctx context.Context) error {
			return prepareCatalogRuntimeMetadata(ctx, metadataRepository.GetExecutor(ctx))
		},
		transactionManager.WithTransaction,
	)
	output, err := catalogreconcile.Reconcile(ctx, authority, securityRepository, store,
		metadataTransaction, metadataReader, reconcileRequest)
	if err != nil {
		return err
	}
	if config.PhaseOnePublication != nil {
		if _, err := catalogreconcile.PublishPhaseOneConsumerLineage(
			ctx,
			connection.DB(),
			config.ArtifactStoreRoot,
			config.AppID,
			config.RuntimeAudience,
			config.PhaseOnePublication.CommandID,
			output,
		); err != nil {
			return fmt.Errorf("publish phase-one Catalog consumer lineage: %w", err)
		}
	}
	outputRaw, outputDigest, err := catalogreconcile.MarshalOutput(output)
	if err != nil {
		return err
	}
	if err := writeAtomic(*outputPath, append(outputRaw, '\n')); err != nil {
		return err
	}
	_, _ = fmt.Fprintf(os.Stdout, "%s\n", outputDigest)
	return nil
}

// prepareCatalogRuntimeMetadata reconstructs the exact persisted runtime
// declarations before Catalog reads _System_Field. The Catalog reconciler is a
// short-lived standalone process, so it cannot inherit the classifier prepared
// by setup or by a serving process. Rehydrating from the installed-pack rows
// keeps provenance bound to durable deployment authority and avoids reopening
// mutable pack sources during publication.
func prepareCatalogRuntimeMetadata(ctx context.Context, executor persistence.Executor) error {
	initializer := bootstrapimpl.NewFieldProvenanceInitializerImpl()
	packs.SetFieldProvenanceInitializer(initializer)
	return packs.RegisterPersistedInstalledRuntimeMetadataWithExecutor(ctx, executor)
}

// withCatalogRuntimeMetadataPreparation binds provenance reconstruction to the
// live metadata transaction path. Reconcile does not invoke this runner when a
// retry can recover from its committed immutable artifacts, so broken current
// installed-pack declarations cannot block that recovery path.
func withCatalogRuntimeMetadataPreparation(
	prepare func(context.Context) error,
	withinTransaction catalogreconcile.InTransaction,
) catalogreconcile.InTransaction {
	return func(ctx context.Context, read func(context.Context) error) error {
		return withinTransaction(ctx, func(txCtx context.Context) error {
			if err := prepare(txCtx); err != nil {
				return fmt.Errorf("prepare Catalog runtime metadata: %w", err)
			}
			return read(txCtx)
		})
	}
}

func runWorkOwnership(args []string) error {
	flags := flag.NewFlagSet("catalog-reconcile work-ownership", flag.ContinueOnError)
	configPath := flags.String("config", "", "path to the immutable Runtime work ownership activation config")
	outputPath := flags.String("output", "", "path for the canonical activation output")
	if err := flags.Parse(args); err != nil {
		return err
	}
	if flags.NArg() != 0 {
		return errors.New("Runtime work ownership activation does not accept positional arguments")
	}
	if *configPath == "" || *outputPath == "" {
		return errors.New("both -config and -output are required")
	}
	rawConfig, err := os.ReadFile(*configPath)
	if err != nil {
		return fmt.Errorf("read Runtime work ownership activation config: %w", err)
	}
	config, err := runtimework.ParseConfig(rawConfig)
	if err != nil {
		return err
	}
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()
	connection, err := database.NewConnection()
	if err != nil {
		return fmt.Errorf("connect TiDB: %w", err)
	}
	defer connection.Close()
	control, err := persistence.NewRuntimeWorkOwnershipControl(connection.DB())
	if err != nil {
		return err
	}
	output, err := runtimework.Apply(ctx, control.Activate, config)
	if err != nil {
		return err
	}
	outputRaw, outputDigest, err := runtimework.MarshalOutput(output)
	if err != nil {
		return err
	}
	if err := writeAtomic(*outputPath, append(outputRaw, '\n')); err != nil {
		return err
	}
	_, _ = fmt.Fprintf(os.Stdout, "%s\n", outputDigest)
	return nil
}

func writeAtomic(path string, raw []byte) error {
	directory := filepath.Dir(path)
	if err := os.MkdirAll(directory, 0o750); err != nil {
		return fmt.Errorf("create output directory: %w", err)
	}
	temp, err := os.CreateTemp(directory, ".catalog-reconcile-output-*")
	if err != nil {
		return fmt.Errorf("create output staging file: %w", err)
	}
	name := temp.Name()
	defer os.Remove(name)
	if _, err := temp.Write(raw); err != nil {
		_ = temp.Close()
		return fmt.Errorf("write output: %w", err)
	}
	if err := temp.Sync(); err != nil {
		_ = temp.Close()
		return fmt.Errorf("sync output: %w", err)
	}
	if err := temp.Chmod(0o440); err != nil {
		_ = temp.Close()
		return fmt.Errorf("protect output: %w", err)
	}
	if err := temp.Close(); err != nil {
		return fmt.Errorf("close output: %w", err)
	}
	if err := os.Rename(name, path); err != nil {
		return fmt.Errorf("publish output: %w", err)
	}
	dir, err := os.Open(directory)
	if err != nil {
		return fmt.Errorf("open output directory: %w", err)
	}
	defer dir.Close()
	if err := dir.Sync(); err != nil {
		return fmt.Errorf("sync output directory: %w", err)
	}
	return nil
}

// This subcommand is an installation operation on current configuration, not a
// Catalog reconciliation. It never constructs the publisher or Catalog owner.
func runApplicationNavigation(args []string) error {
	flags := flag.NewFlagSet("catalog-reconcile application-navigation", flag.ContinueOnError)
	inputPath := flags.String("input", "", "navigation initialization input")
	outputPath := flags.String("output", "", "committed navigation result")
	assertOnly := flags.Bool("assert-only", false, "validate initialized current navigation without writing")
	validateOnly := flags.Bool("validate-only", false, "validate input without opening a database")
	if err := flags.Parse(args); err != nil {
		return err
	}
	if *validateOnly && *assertOnly {
		return errors.New("application-navigation -validate-only and -assert-only are mutually exclusive")
	}
	if flags.NArg() != 0 || *inputPath == "" || (!*validateOnly && *outputPath == "") {
		return errors.New("application-navigation requires -input and -output and accepts no positional arguments")
	}
	inputFile, err := os.Open(*inputPath)
	if err != nil {
		return err
	}
	defer inputFile.Close()
	input, err := applicationnavigation.DecodeInput(inputFile)
	if err != nil {
		return err
	}
	if *validateOnly {
		return nil
	}
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()
	ctx, cancel := context.WithTimeout(ctx, 45*time.Second)
	defer cancel()
	connection, err := database.NewConnectionContext(ctx, database.PoolOptions{})
	if err != nil {
		return err
	}
	defer connection.Close()
	result, err := applicationnavigation.Apply(ctx, connection, input, *assertOnly)
	if err != nil {
		return err
	}
	raw, err := json.Marshal(result)
	if err != nil {
		return err
	}
	return writeAtomic(*outputPath, append(raw, '\n'))
}
