package clusterrecovery

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"slices"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/pingcap-inc/clinic/pkg/cloudevent"
)

var runIDPattern = regexp.MustCompile(`^[A-Za-z0-9._-]{1,64}$`)

func DecodeSeed(r io.Reader) (*Seed, error) {
	var seed Seed
	if err := decodeStrictBoundedJSON(r, MaxSeedBytes, &seed); err != nil {
		return nil, fmt.Errorf("decode recovery seed: %w", err)
	}
	if err := ValidateSeed(&seed); err != nil {
		return nil, err
	}
	canonicalizeSeed(&seed)
	return &seed, nil
}

func DecodeManifest(r io.Reader) (*Manifest, error) {
	var manifest Manifest
	if err := decodeStrictBoundedJSON(r, MaxSeedBytes, &manifest); err != nil {
		return nil, fmt.Errorf("decode recovery manifest: %w", err)
	}
	return &manifest, nil
}

func decodeStrictBoundedJSON(r io.Reader, maxBytes int64, dst any) error {
	if r == nil {
		return errors.New("input is required")
	}
	limited := io.LimitReader(r, maxBytes+1)
	data, err := io.ReadAll(limited)
	if err != nil {
		return err
	}
	if int64(len(data)) > maxBytes {
		return fmt.Errorf("input exceeds %d bytes", maxBytes)
	}
	decoder := json.NewDecoder(bytes.NewReader(data))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(dst); err != nil {
		return err
	}
	var extra any
	if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
		if err == nil {
			return errors.New("multiple JSON values are not allowed")
		}
		return err
	}
	return nil
}

func ValidateSeed(seed *Seed) error {
	if seed == nil {
		return errors.New("seed is required")
	}
	if seed.SchemaVersion != SchemaVersion {
		return fmt.Errorf("unsupported seed schema version %q", seed.SchemaVersion)
	}
	if seed.Environment != StagingEnvironment {
		return fmt.Errorf("recovery seed environment must be %q", StagingEnvironment)
	}
	if err := validateWindow(seed.IncidentStart, seed.IncidentEnd); err != nil {
		return err
	}
	if len(seed.Candidates) == 0 {
		return errors.New("seed must contain at least one candidate")
	}
	if len(seed.Candidates) > MaxSeedCandidates {
		return fmt.Errorf("seed contains %d candidates; maximum is %d", len(seed.Candidates), MaxSeedCandidates)
	}
	for i := range seed.Candidates {
		candidate := &seed.Candidates[i]
		if _, err := parsePositiveID(candidate.ClusterID); err != nil {
			return fmt.Errorf("candidate %d cluster_id: %w", i, err)
		}
		if candidate.TenantID == 0 &&
			(candidate.ClusterType != cloudevent.ClusterTypeTidbxResourcePool || candidate.Evidence != EvidenceClusterEventDLQ) {
			return fmt.Errorf("candidate %d tenant_id may be zero only for a resource-pool ClusterEvent DLQ candidate", i)
		}
		if len(candidate.ClusterType) > 64 {
			return fmt.Errorf("candidate %d cluster_type exceeds 64 bytes", i)
		}
		if !validEvidence(candidate.Evidence) {
			return fmt.Errorf("candidate %d has unsupported evidence %q", i, candidate.Evidence)
		}
		if candidate.EventCreatedAt.IsZero() {
			return fmt.Errorf("candidate %d event_created_at is required", i)
		}
		observed := candidate.EventCreatedAt.UTC()
		if observed.Before(seed.IncidentStart.UTC()) || observed.After(seed.IncidentEnd.UTC()) {
			return fmt.Errorf("candidate %d event_created_at is outside the incident window", i)
		}
	}
	return nil
}

func validateWindow(start, end time.Time) error {
	if start.IsZero() || end.IsZero() {
		return errors.New("incident_start and incident_end are required")
	}
	if !start.Before(end) {
		return errors.New("incident_start must be before incident_end")
	}
	if end.Sub(start) > MaxIncidentWindow {
		return fmt.Errorf("incident window exceeds %s", MaxIncidentWindow)
	}
	return nil
}

func validEvidence(evidence EvidenceType) bool {
	return evidence == EvidenceClusterEventDLQ ||
		evidence == EvidenceImportDLQ ||
		evidence == EvidenceSourceCompare
}

func parsePositiveID(value string) (uint64, error) {
	if value == "" || strings.TrimSpace(value) != value {
		return 0, errors.New("must be an unpadded decimal string")
	}
	id, err := strconv.ParseUint(value, 10, 64)
	if err != nil || id == 0 {
		return 0, errors.New("must be a positive uint64 decimal string")
	}
	if strconv.FormatUint(id, 10) != value {
		return 0, errors.New("must use canonical decimal formatting")
	}
	return id, nil
}

func canonicalizeSeed(seed *Seed) {
	seed.IncidentStart = seed.IncidentStart.UTC()
	seed.IncidentEnd = seed.IncidentEnd.UTC()
	for i := range seed.Candidates {
		seed.Candidates[i].EventCreatedAt = seed.Candidates[i].EventCreatedAt.UTC()
	}
	sort.SliceStable(seed.Candidates, func(i, j int) bool {
		left, right := seed.Candidates[i], seed.Candidates[j]
		if left.ClusterID != right.ClusterID {
			return left.ClusterID < right.ClusterID
		}
		if left.TenantID != right.TenantID {
			return left.TenantID < right.TenantID
		}
		if left.ClusterType != right.ClusterType {
			return left.ClusterType < right.ClusterType
		}
		return left.Evidence < right.Evidence
	})
}

func FinalizeManifest(manifest *Manifest) error {
	if manifest == nil {
		return errors.New("manifest is required")
	}
	canonicalizeManifest(manifest)
	manifest.ManifestHash = ""
	if err := validateManifestShape(manifest); err != nil {
		return err
	}
	hash, err := manifestDigest(manifest)
	if err != nil {
		return err
	}
	manifest.ManifestHash = hash
	return nil
}

func VerifyManifest(manifest *Manifest, expectedHash string, now time.Time) error {
	if manifest == nil {
		return errors.New("manifest is required")
	}
	if err := validateManifestShape(manifest); err != nil {
		return err
	}
	if !isSHA256(manifest.ManifestHash) {
		return errors.New("manifest_hash must be a lowercase SHA-256 digest")
	}
	if expectedHash == "" || !isSHA256(expectedHash) {
		return errors.New("expected manifest hash must be a lowercase SHA-256 digest")
	}
	if manifest.ManifestHash != expectedHash {
		return errors.New("expected manifest hash does not match manifest_hash")
	}
	canonical := cloneManifest(manifest)
	canonicalizeManifest(canonical)
	actualShape, err := json.Marshal(manifest)
	if err != nil {
		return err
	}
	canonicalShape, err := json.Marshal(canonical)
	if err != nil {
		return err
	}
	if !bytes.Equal(actualShape, canonicalShape) {
		return errors.New("manifest content is not in canonical execution order")
	}
	actual, err := manifestDigest(manifest)
	if err != nil {
		return err
	}
	if actual != manifest.ManifestHash {
		return errors.New("manifest content hash verification failed")
	}
	if !now.Before(manifest.ExpiresAt) {
		return errors.New("manifest is expired")
	}
	if now.Before(manifest.GeneratedAt.Add(-5 * time.Minute)) {
		return errors.New("manifest generation time is in the future")
	}
	return nil
}

func cloneManifest(manifest *Manifest) *Manifest {
	copyManifest := *manifest
	copyManifest.Actions = append([]Action(nil), manifest.Actions...)
	for index := range copyManifest.Actions {
		copyManifest.Actions[index].Fields = append([]string(nil), manifest.Actions[index].Fields...)
		copyManifest.Actions[index].DependencyIDs = append([]string(nil), manifest.Actions[index].DependencyIDs...)
	}
	return &copyManifest
}

func validateManifestShape(manifest *Manifest) error {
	if manifest.SchemaVersion != SchemaVersion {
		return fmt.Errorf("unsupported manifest schema version %q", manifest.SchemaVersion)
	}
	if manifest.Environment != StagingEnvironment {
		return fmt.Errorf("manifest environment must be %q", StagingEnvironment)
	}
	if !runIDPattern.MatchString(manifest.RunID) {
		return errors.New("run_id must match [A-Za-z0-9._-]{1,64}")
	}
	if manifest.MutationClass != MutationCreate && manifest.MutationClass != MutationUpdate {
		return fmt.Errorf("unsupported mutation_class %q", manifest.MutationClass)
	}
	if err := validateWindow(manifest.IncidentStart, manifest.IncidentEnd); err != nil {
		return err
	}
	if manifest.GeneratedAt.IsZero() || manifest.ExpiresAt.IsZero() {
		return errors.New("generated_at and expires_at are required")
	}
	if !manifest.GeneratedAt.Before(manifest.ExpiresAt) {
		return errors.New("generated_at must be before expires_at")
	}
	if manifest.ExpiresAt.Sub(manifest.GeneratedAt) > MaxManifestLifetime {
		return fmt.Errorf("manifest lifetime exceeds %s", MaxManifestLifetime)
	}
	if len(manifest.Actions) > MaxManifestActions {
		return fmt.Errorf("manifest contains %d actions; maximum is %d", len(manifest.Actions), MaxManifestActions)
	}
	seen := make(map[string]struct{}, len(manifest.Actions))
	for i := range manifest.Actions {
		action := &manifest.Actions[i]
		if action.Kind != ObjectKindMgmtCluster && action.Kind != ObjectKindResourcePool {
			return fmt.Errorf("action %d has unsupported kind %q", i, action.Kind)
		}
		if _, err := parsePositiveID(action.ClusterID); err != nil {
			return fmt.Errorf("action %d cluster_id: %w", i, err)
		}
		if action.TenantID == 0 &&
			(action.Kind != ObjectKindResourcePool || action.ClusterType != cloudevent.ClusterTypeTidbxResourcePool) {
			return fmt.Errorf("action %d tenant_id may be zero only for a resource-pool action", i)
		}
		key := action.ClusterID
		if _, exists := seen[key]; exists {
			return fmt.Errorf("action %d duplicates cluster_id %s", i, key)
		}
		seen[key] = struct{}{}
		if action.SourceUpdatedAt.IsZero() {
			return fmt.Errorf("action %d source_updated_at is required", i)
		}
		if !isSHA256(action.DesiredChecksum) {
			return fmt.Errorf("action %d desired_checksum must be a lowercase SHA-256 digest", i)
		}
		if manifest.MutationClass == MutationCreate {
			if action.BeforeChecksum != "" || len(action.Fields) != 0 {
				return fmt.Errorf("create action %d must not include before_checksum or fields", i)
			}
		} else {
			if !isSHA256(action.BeforeChecksum) {
				return fmt.Errorf("update action %d before_checksum must be a lowercase SHA-256 digest", i)
			}
			if len(action.Fields) == 0 {
				return fmt.Errorf("update action %d must include fields", i)
			}
			if !sortedUniqueStrings(action.Fields) {
				return fmt.Errorf("update action %d fields must be sorted and unique", i)
			}
		}
		if !sortedUniqueIDs(action.DependencyIDs) {
			return fmt.Errorf("action %d dependency_ids must be sorted unique uint64 strings", i)
		}
	}
	return nil
}

func manifestDigest(manifest *Manifest) (string, error) {
	copyManifest := cloneManifest(manifest)
	copyManifest.ManifestHash = ""
	canonicalizeManifest(copyManifest)
	payload, err := json.Marshal(copyManifest)
	if err != nil {
		return "", fmt.Errorf("marshal manifest for hashing: %w", err)
	}
	sum := sha256.Sum256(payload)
	return hex.EncodeToString(sum[:]), nil
}

func canonicalizeManifest(manifest *Manifest) {
	manifest.IncidentStart = manifest.IncidentStart.UTC()
	manifest.IncidentEnd = manifest.IncidentEnd.UTC()
	manifest.GeneratedAt = manifest.GeneratedAt.UTC()
	manifest.ExpiresAt = manifest.ExpiresAt.UTC()
	for i := range manifest.Actions {
		action := &manifest.Actions[i]
		action.SourceUpdatedAt = action.SourceUpdatedAt.UTC()
		slices.Sort(action.Fields)
		action.Fields = slices.Compact(action.Fields)
		slices.Sort(action.DependencyIDs)
		action.DependencyIDs = slices.Compact(action.DependencyIDs)
	}
	sort.SliceStable(manifest.Actions, func(i, j int) bool {
		left, right := manifest.Actions[i], manifest.Actions[j]
		if left.Kind != right.Kind {
			return left.Kind == ObjectKindResourcePool
		}
		if left.ClusterID != right.ClusterID {
			return left.ClusterID < right.ClusterID
		}
		return left.TenantID < right.TenantID
	})
}

func sortedUniqueStrings(values []string) bool {
	for i, value := range values {
		if value == "" || (i > 0 && values[i-1] >= value) {
			return false
		}
	}
	return true
}

func sortedUniqueIDs(values []string) bool {
	if !sortedUniqueStrings(values) {
		return len(values) == 0
	}
	for _, value := range values {
		if _, err := parsePositiveID(value); err != nil {
			return false
		}
	}
	return true
}

func isSHA256(value string) bool {
	if len(value) != sha256.Size*2 || strings.ToLower(value) != value {
		return false
	}
	_, err := hex.DecodeString(value)
	return err == nil
}

func WriteJSONArtifact(path string, value any) error {
	if path == "" {
		return errors.New("artifact path is required")
	}
	dir := filepath.Dir(path)
	info, err := os.Stat(dir)
	if err != nil {
		return fmt.Errorf("stat artifact directory: %w", err)
	}
	if !info.IsDir() {
		return errors.New("artifact parent is not a directory")
	}
	if _, err := os.Lstat(path); err == nil {
		return errors.New("artifact already exists")
	} else if !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("inspect artifact path: %w", err)
	}
	payload, err := json.MarshalIndent(value, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal artifact: %w", err)
	}
	payload = append(payload, '\n')
	tmp, err := os.CreateTemp(dir, ".clinic-recovery-*")
	if err != nil {
		return fmt.Errorf("create temporary artifact: %w", err)
	}
	tmpPath := tmp.Name()
	cleanup := func() {
		_ = tmp.Close()
		_ = os.Remove(tmpPath)
	}
	if err := tmp.Chmod(0o600); err != nil {
		cleanup()
		return fmt.Errorf("set temporary artifact mode: %w", err)
	}
	if _, err := tmp.Write(payload); err != nil {
		cleanup()
		return fmt.Errorf("write temporary artifact: %w", err)
	}
	if err := tmp.Sync(); err != nil {
		cleanup()
		return fmt.Errorf("sync temporary artifact: %w", err)
	}
	if err := tmp.Close(); err != nil {
		cleanup()
		return fmt.Errorf("close temporary artifact: %w", err)
	}
	if err := os.Link(tmpPath, path); err != nil {
		cleanup()
		return fmt.Errorf("publish artifact: %w", err)
	}
	if err := os.Remove(tmpPath); err != nil {
		_ = os.Remove(path)
		return fmt.Errorf("remove temporary artifact: %w", err)
	}
	directory, err := os.Open(dir)
	if err != nil {
		return fmt.Errorf("open artifact directory for sync: %w", err)
	}
	if err := directory.Sync(); err != nil {
		_ = directory.Close()
		return fmt.Errorf("sync artifact directory: %w", err)
	}
	if err := directory.Close(); err != nil {
		return fmt.Errorf("close artifact directory: %w", err)
	}
	return nil
}
