//nolint:gocritic // Journal fingerprints deliberately copy immutable activation contracts before persistence.
package byoc

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strconv"
	"strings"
	"time"
)

const (
	stateDirectoryName = "byoc"
	journalFileName    = "activation.json"
	lockFileName       = "activation.lock"
)

type Journal struct {
	APIVersion            string                 `json:"apiVersion"`
	Kind                  string                 `json:"kind"`
	Session               string                 `json:"session"`
	Fingerprint           string                 `json:"fingerprint"`
	Provenance            *ActivationProvenance  `json:"provenance,omitempty"`
	State                 ActivationState        `json:"state"`
	Preflight             *PreflightResult       `json:"preflight,omitempty"`
	RemoteRuntimeHostGone *bool                  `json:"remote_runtime_host_gone,omitempty"`
	Stages                map[Stage]*StageRecord `json:"stages"`
	CreatedAt             time.Time              `json:"created_at"`
	UpdatedAt             time.Time              `json:"updated_at"`
}

// ActivationProvenance is the code-owned part of the immutable activation
// contract. Destroy uses the journaled values so a later reviewed bundle or
// image-closure bump can require destroy/recreate without making teardown of
// the already-bound session impossible.
type ActivationProvenance struct {
	BundleCommit       string `json:"bundle_commit"`
	BundleDigest       string `json:"bundle_digest"`
	ImageClosureDigest string `json:"image_closure_digest"`
}

func activationProvenance(input Input) *ActivationProvenance {
	return &ActivationProvenance{
		BundleCommit:       strings.TrimSpace(input.BundleCommit),
		BundleDigest:       strings.TrimSpace(input.BundleDigest),
		ImageClosureDigest: strings.TrimSpace(input.ImageClosureDigest),
	}
}

type StageRecord struct {
	State       StageState        `json:"state"`
	Attempts    int               `json:"attempts"`
	StartedAt   *time.Time        `json:"started_at,omitempty"`
	CompletedAt *time.Time        `json:"completed_at,omitempty"`
	Message     string            `json:"message,omitempty"`
	Artifacts   map[string]string `json:"artifacts,omitempty"`
}

type fingerprintContract struct {
	Session               string             `json:"session"`
	Owner                 string             `json:"owner"`
	Region                string             `json:"region"`
	ControlPlaneAccountID string             `json:"control_plane_account_id"`
	CustomerAccountID     string             `json:"customer_account_id"`
	TenantID              string             `json:"tenant_id"`
	AvailabilityZones     []AvailabilityZone `json:"availability_zones"`
	TiDBCIDR              string             `json:"tidb_cidr"`
	TiDBHostedZoneName    string             `json:"tidb_hosted_zone_name"`
	TiDBHostedZoneID      string             `json:"tidb_hosted_zone_id"`
	O11YHostedZoneName    string             `json:"o11y_hosted_zone_name"`
	O11YHostedZoneID      string             `json:"o11y_hosted_zone_id"`
	PCAARN                string             `json:"pca_arn"`
	ImageSyncRegion       string             `json:"image_sync_region"`
	ClinicAccountID       string             `json:"clinic_account_id"`
	ReuseAccountSetup     bool               `json:"reuse_account_setup"`
	RemoteInstanceID      string             `json:"remote_instance_id"`
	RemoteRuntimeRoleARN  string             `json:"remote_runtime_role_arn"`
	RemoteSecurityGroupID string             `json:"remote_security_group_id"`
	RemoteSubnetID        string             `json:"remote_subnet_id"`
	BundleCommit          string             `json:"bundle_commit"`
	BundleDigest          string             `json:"bundle_digest"`
	ImageClosureDigest    string             `json:"image_closure_digest"`
}

func Fingerprint(input Input) (string, error) {
	tenantID, err := canonicalTenantID(input.TenantID)
	if err != nil {
		return "", err
	}
	zones := append([]AvailabilityZone(nil), input.AvailabilityZones...)
	slices.SortFunc(zones, func(a, b AvailabilityZone) int {
		if a.ID == b.ID {
			return strings.Compare(a.Name, b.Name)
		}
		return strings.Compare(a.ID, b.ID)
	})
	contract := fingerprintContract{
		Session:               strings.TrimSpace(input.Session),
		Owner:                 strings.TrimSpace(input.Owner),
		Region:                strings.TrimSpace(input.Region),
		ControlPlaneAccountID: strings.TrimSpace(input.ControlPlaneAccountID),
		CustomerAccountID:     strings.TrimSpace(input.CustomerAccountID),
		TenantID:              tenantID,
		AvailabilityZones:     zones,
		TiDBCIDR:              strings.TrimSpace(input.TiDBCIDR),
		TiDBHostedZoneName:    strings.TrimSpace(input.TiDBHostedZoneName),
		TiDBHostedZoneID:      strings.TrimSpace(input.TiDBHostedZoneID),
		O11YHostedZoneName:    strings.TrimSpace(input.O11YHostedZoneName),
		O11YHostedZoneID:      strings.TrimSpace(input.O11YHostedZoneID),
		PCAARN:                strings.TrimSpace(input.PCAARN),
		ImageSyncRegion:       strings.TrimSpace(input.ImageSyncRegion),
		ClinicAccountID:       strings.TrimSpace(input.ClinicAccountID),
		ReuseAccountSetup:     input.ReuseAccountSetup,
		RemoteInstanceID:      strings.TrimSpace(input.RemoteInstanceID),
		RemoteRuntimeRoleARN:  strings.TrimSpace(input.RemoteRuntimeRoleARN),
		RemoteSecurityGroupID: strings.TrimSpace(input.RemoteSecurityGroupID),
		RemoteSubnetID:        strings.TrimSpace(input.RemoteSubnetID),
		BundleCommit:          strings.TrimSpace(input.BundleCommit),
		BundleDigest:          strings.TrimSpace(input.BundleDigest),
		ImageClosureDigest:    strings.TrimSpace(input.ImageClosureDigest),
	}
	if contract.Session == "" || contract.Region == "" || contract.ControlPlaneAccountID == "" ||
		contract.CustomerAccountID == "" || contract.BundleDigest == "" {
		return "", errors.New("incomplete BYOC fingerprint contract")
	}
	data, err := json.Marshal(contract)
	if err != nil {
		return "", fmt.Errorf("marshal BYOC fingerprint contract: %w", err)
	}
	digest := sha256.Sum256(data)
	return "sha256:" + hex.EncodeToString(digest[:]), nil
}

func canonicalTenantID(value string) (string, error) {
	trimmed := strings.TrimSpace(value)
	if trimmed != value {
		return "", errors.New("BYOC tenant ID must use canonical base-10 form without leading zeros")
	}
	value = trimmed
	parsed, err := strconv.ParseUint(value, 10, 64)
	if err != nil || parsed == 0 {
		return "", errors.New("BYOC tenant ID must be a positive integer")
	}
	canonical := strconv.FormatUint(parsed, 10)
	if canonical != value {
		return "", errors.New("BYOC tenant ID must use canonical base-10 form without leading zeros")
	}
	return canonical, nil
}

func StateDir(sessionStateDir string) string {
	return filepath.Join(sessionStateDir, stateDirectoryName)
}

func JournalPath(sessionStateDir string) string {
	return filepath.Join(StateDir(sessionStateDir), journalFileName)
}

func LockPath(sessionStateDir string) string {
	return filepath.Join(StateDir(sessionStateDir), lockFileName)
}

func LoadJournal(sessionStateDir string) (*Journal, error) {
	data, err := os.ReadFile(JournalPath(sessionStateDir))
	if err != nil {
		return nil, err
	}
	var journal Journal
	if err := json.Unmarshal(data, &journal); err != nil {
		return nil, fmt.Errorf("parse BYOC activation journal: %w", err)
	}
	if journal.APIVersion != JournalAPIVersion || journal.Kind != JournalKind {
		return nil, fmt.Errorf("unsupported BYOC activation journal %q %q", journal.APIVersion, journal.Kind)
	}
	if journal.Stages == nil {
		journal.Stages = make(map[Stage]*StageRecord)
	}
	return &journal, nil
}

func saveJournal(sessionStateDir string, journal *Journal) error {
	if journal == nil {
		return errors.New("BYOC activation journal is required")
	}
	dir := StateDir(sessionStateDir)
	if err := os.MkdirAll(dir, 0o700); err != nil {
		return fmt.Errorf("create BYOC state directory: %w", err)
	}
	journal.UpdatedAt = time.Now().UTC()
	data, err := json.MarshalIndent(journal, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal BYOC activation journal: %w", err)
	}
	data = append(data, '\n')
	tmp, err := os.CreateTemp(dir, ".activation-*.tmp")
	if err != nil {
		return fmt.Errorf("create BYOC activation journal temp file: %w", err)
	}
	tmpPath := tmp.Name()
	defer func() { _ = os.Remove(tmpPath) }()
	if err := tmp.Chmod(0o600); err != nil {
		_ = tmp.Close()
		return fmt.Errorf("secure BYOC activation journal temp file: %w", err)
	}
	if _, err := tmp.Write(data); err != nil {
		_ = tmp.Close()
		return fmt.Errorf("write BYOC activation journal: %w", err)
	}
	if err := tmp.Sync(); err != nil {
		_ = tmp.Close()
		return fmt.Errorf("sync BYOC activation journal: %w", err)
	}
	if err := tmp.Close(); err != nil {
		return fmt.Errorf("close BYOC activation journal: %w", err)
	}
	if err := os.Rename(tmpPath, JournalPath(sessionStateDir)); err != nil {
		return fmt.Errorf("publish BYOC activation journal: %w", err)
	}
	return nil
}
