package status

import (
	"os"
	"path/filepath"
	"time"

	"github.com/tidbcloud/observability/vm-migration/internal/jsonio"
	"github.com/tidbcloud/observability/vm-migration/internal/schema"
)

// UTCNow returns current UTC time.
func UTCNow() time.Time {
	return time.Now().UTC()
}

// LoadJSON reads and parses a JSON file into v.
func LoadJSON(path string, v any) error {
	return jsonio.ReadFile(path, v)
}

// LoadDocument loads a status document from disk.
func LoadDocument(path string) (*schema.Document, error) {
	doc := &schema.Document{}
	if err := LoadJSON(path, doc); err != nil {
		return nil, err
	}
	return doc, nil
}

// LoadManifest loads a manifest from disk.
func LoadManifest(path string) (*schema.Manifest, error) {
	m := &schema.Manifest{}
	if err := LoadJSON(path, m); err != nil {
		return nil, err
	}
	return m, nil
}

// IterShards yields each manifest shard with its parent tenant attached.
func IterShards(m *schema.Manifest) []schema.Shard {
	out := make([]schema.Shard, 0, len(m.Tenants))
	for _, t := range m.Tenants {
		for _, s := range t.Shards {
			out = append(out, schema.Shard{
				ShardID:   s.ShardID,
				Tenant:    t.Tenant,
				TimeStart: s.TimeStart,
				TimeEnd:   s.TimeEnd,
			})
		}
	}
	return out
}

// FindShard returns a pointer to the shard with id within doc, or nil if no such
// shard exists. The pointer aliases doc.Shards, so callers may mutate through it.
func FindShard(doc *schema.Document, shardID string) *schema.Shard {
	for i := range doc.Shards {
		if doc.Shards[i].ShardID == shardID {
			return &doc.Shards[i]
		}
	}
	return nil
}

// BuildSummary computes the top-level summary from a document's shards.
func BuildSummary(doc *schema.Document) schema.Summary {
	counts := map[schema.Phase]int{}
	var totalBytes int64
	verified := 0
	for _, s := range doc.Shards {
		phase := s.Phase
		if phase == "" {
			phase = schema.PhaseUnknown
		}
		counts[phase]++
		if s.Verified {
			verified++
		}
		if s.Phase == schema.PhaseImported && s.Bytes != nil {
			totalBytes += *s.Bytes
		}
	}
	total := len(doc.Shards)
	imported := counts[schema.PhaseImported]
	failed := counts[schema.PhaseFailed]
	return schema.Summary{
		UpdatedAt:  UTCNow(),
		Total:      total,
		Imported:   imported,
		Verified:   verified,
		Failed:     failed,
		Pending:    total - imported - failed,
		TotalBytes: totalBytes,
	}
}

// RefreshSummary recomputes doc.Summary in place.
func RefreshSummary(doc *schema.Document) {
	doc.Summary = BuildSummary(doc)
}

// WriteJSONAtomic marshals doc, refreshes summary, and writes via tmp+rename.
func WriteJSONAtomic(path string, doc *schema.Document) error {
	RefreshSummary(doc)
	return writeAtomic(path, doc)
}

// DumpJSONAtomic writes arbitrary JSON-serializable value via tmp+rename.
func DumpJSONAtomic(path string, v any) error {
	return writeAtomic(path, v)
}

// writeAtomic marshals v without HTML escaping and renames a tmp file into
// place. The tmp+rename pair gives atomicity: readers see either the old or
// the new file, never a partial write. tmp.Sync plus a directory fsync make
// the rename survive power loss — without them the new file can vanish or
// zero-fill after a crash despite the rename having "succeeded".
func writeAtomic(path string, v any) error {
	data, err := jsonio.MarshalIndent(v)
	if err != nil {
		return err
	}
	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}
	tmp, err := os.CreateTemp(dir, filepath.Base(path)+".tmp.*")
	if err != nil {
		return err
	}
	tmpName := tmp.Name()
	defer os.Remove(tmpName)
	if _, err := tmp.Write(data); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Sync(); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	if err := os.Rename(tmpName, path); err != nil {
		return err
	}
	return syncDir(dir)
}

// syncDir flushes a directory entry so a just-renamed file survives power
// loss. Errors on filesystems that do not support directory fsync are
// returned as-is; callers on such filesystems accept the pre-fsync crash
// window.
func syncDir(dir string) error {
	f, err := os.Open(dir)
	if err != nil {
		return err
	}
	defer f.Close()
	return f.Sync()
}

// PathForManifest derives the status JSON path from a manifest path:
// <stem>-manifest.json → <stem>-status.json.
func PathForManifest(manifestPath string) string {
	return schema.SwapSuffix(manifestPath, "-manifest", "-status.json")
}

// BuildInitialStatus constructs a fresh schema.Document from a manifest; every
// shard phase=pending.
func BuildInitialStatus(m *schema.Manifest, manifestRef string) *schema.Document {
	shards := make([]schema.Shard, 0)
	for _, t := range m.Tenants {
		for _, s := range t.Shards {
			shards = append(shards, schema.Shard{
				ShardID:   s.ShardID,
				Tenant:    t.Tenant,
				TimeStart: s.TimeStart,
				TimeEnd:   s.TimeEnd,
				Phase:     schema.PhasePending,
			})
		}
	}
	return &schema.Document{
		ManifestRef: manifestRef,
		Region:      m.Region,
		Namespace:   m.Namespace,
		Vmcluster:   m.Vmcluster,
		Shards:      shards,
	}
}

// FormatTime formats a time.Time as ISO 8601 with Z suffix (RFC3339Nano with
// explicit UTC, e.g. 2026-08-08T08:51:31.629Z).
func FormatTime(t time.Time) string {
	return t.UTC().Format(time.RFC3339Nano)
}

// ParseTime parses an ISO 8601 timestamp. Accepts both Z and +00:00 suffixes.
func ParseTime(s string) (time.Time, error) {
	return time.Parse(time.RFC3339Nano, s)
}
