package control

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"sync"
	"time"

	"github.com/google/uuid"
)

type persistedState struct {
	Closes   map[string]*CloseRun `json:"closes"`
	Attempts []*Attempt           `json:"attempts"`
}

type Store struct {
	mu    sync.RWMutex
	path  string
	state persistedState
}

func NewStore(path string) (*Store, error) {
	return NewStoreForMode(path, "observe")
}

func NewStoreForMode(path, mode string) (*Store, error) {
	store := &Store{
		path: path,
		state: persistedState{
			Closes:   make(map[string]*CloseRun),
			Attempts: make([]*Attempt, 0),
		},
	}
	if err := store.load(); err != nil {
		return nil, err
	}
	if mode != "demo" {
		for _, close := range store.state.Closes {
			for _, step := range close.Steps {
				if step.Receipt["source"] == "seeded local preview" || step.Receipt["demo"] == true {
					return nil, fmt.Errorf("demo state cannot be opened in a real runtime; use an isolated state path")
				}
			}
		}
	}
	if mode == "demo" && len(store.state.Closes) == 0 {
		store.seedDemoClose()
		if err := store.persistLocked(); err != nil {
			return nil, err
		}
	}
	recovered := false
	for _, attempt := range store.state.Attempts {
		if attempt.FinishedAt == nil && attempt.Execution == ExecutionRunning {
			attempt.Execution = ExecutionUnknown
			if close := store.state.Closes[attempt.CloseRunID]; close != nil {
				if step := close.Steps[attempt.StepKey]; step != nil {
					step.Execution = ExecutionUnknown
					step.Error = "进程重启；原运行结果未知，必须按原身份对账"
				}
				deriveClose(close)
			}
			recovered = true
		}
	}
	if recovered {
		if err := store.persistLocked(); err != nil {
			return nil, err
		}
	}
	return store, nil
}

func (s *Store) load() error {
	payload, err := os.ReadFile(s.path)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err != nil {
		return fmt.Errorf("read control store: %w", err)
	}
	if err := json.Unmarshal(payload, &s.state); err != nil {
		return fmt.Errorf("decode control store: %w", err)
	}
	if s.state.Closes == nil {
		s.state.Closes = make(map[string]*CloseRun)
	}
	return nil
}

func (s *Store) seedDemoClose() {
	now := time.Now().UTC().Truncate(time.Second)
	closeRun := newCloseRun("2026-08", "actual", now)
	for _, key := range []string{"ar_table_ready", "sync_ar_to_bigquery", "prepare_crm_references", "verify_auxiliary_inputs", "prepare_dbt_billing_inputs", "verify_billing_inputs", "freeze_billing_inputs"} {
		step := closeRun.Steps[key]
		step.Execution = ExecutionSucceeded
		step.Verification = VerificationPassed
		step.AttemptCount = 1
		step.LastSucceededAt = &now
		step.LastVerifiedAt = &now
		step.ObservedAt = &now
		step.Receipt = map[string]any{"source": "seeded local preview", "verified": true}
	}
	deriveClose(closeRun)
	s.state.Closes[closeRun.ID] = closeRun
}

func newCloseRun(billMonth, mode string, now time.Time) *CloseRun {
	id := fmt.Sprintf("fc_%s_%s_%s", billMonth[0:4]+billMonth[5:7], mode, uuid.NewString()[0:8])
	steps := make(map[string]*StepState, len(MonthlyCloudClose))
	for _, definition := range MonthlyCloudClose {
		steps[definition.Key] = &StepState{
			Key:          definition.Key,
			Execution:    ExecutionPending,
			Verification: VerificationNotChecked,
			Receipt:      map[string]any{},
			Definition:   definition,
		}
	}
	closeRun := &CloseRun{
		ID:             id,
		Profile:        "monthly_cloud_close",
		BillMonth:      billMonth,
		Mode:           mode,
		Status:         "draft",
		DefinitionHash: DefinitionHash(),
		SourceSHA:      envOrValue("FINANCE_PORTAL_BUILD_SHA", "unversioned-local"),
		CreatedAt:      now,
		UpdatedAt:      now,
		Steps:          steps,
	}
	deriveClose(closeRun)
	return closeRun
}

func (s *Store) ListCloses() []*CloseRun {
	s.mu.RLock()
	defer s.mu.RUnlock()
	result := make([]*CloseRun, 0, len(s.state.Closes))
	for _, closeRun := range s.state.Closes {
		copyRun := cloneClose(closeRun)
		deriveClose(copyRun)
		result = append(result, copyRun)
	}
	sort.Slice(result, func(i, j int) bool { return result[i].CreatedAt.After(result[j].CreatedAt) })
	return result
}

func (s *Store) GetClose(id string) (*CloseRun, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	closeRun, ok := s.state.Closes[id]
	if !ok {
		return nil, false
	}
	result := cloneClose(closeRun)
	deriveClose(result)
	return result, true
}

func (s *Store) CreateClose(billMonth, mode string) (*CloseRun, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	for _, closeRun := range s.state.Closes {
		if closeRun.BillMonth == billMonth && closeRun.Mode == mode && closeRun.Status != "completed" && closeRun.Status != "superseded" && closeRun.Status != "aborted" {
			return nil, fmt.Errorf("active close already exists for %s %s", billMonth, mode)
		}
	}
	closeRun := newCloseRun(billMonth, mode, time.Now().UTC())
	s.state.Closes[closeRun.ID] = closeRun
	if err := s.persistLocked(); err != nil {
		delete(s.state.Closes, closeRun.ID)
		return nil, err
	}
	return cloneClose(closeRun), nil
}

func (s *Store) BeginAttempt(closeID, stepKey, action, reason string) (*Attempt, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	closeRun, ok := s.state.Closes[closeID]
	if !ok {
		return nil, fmt.Errorf("close not found")
	}
	step, ok := closeRun.Steps[stepKey]
	if !ok {
		return nil, fmt.Errorf("step not found")
	}
	if step.Execution == ExecutionRunning {
		return nil, fmt.Errorf("step already running")
	}
	before := cloneClose(closeRun)
	attemptCount := len(s.state.Attempts)
	now := time.Now().UTC()
	step.AttemptCount++
	step.Execution = ExecutionRunning
	step.Verification = VerificationNotChecked
	step.Error = ""
	step.ObservedAt = &now
	attempt := &Attempt{
		ID:             uuid.NewString(),
		CloseRunID:     closeID,
		StepKey:        stepKey,
		AttemptNo:      step.AttemptCount,
		Action:         action,
		Reason:         reason,
		IdempotencyKey: fmt.Sprintf("%s:%s:%d", closeID, stepKey, step.AttemptCount),
		Execution:      ExecutionRunning,
		Verification:   VerificationNotChecked,
		StartedAt:      now,
	}
	s.state.Attempts = append(s.state.Attempts, attempt)
	closeRun.UpdatedAt = now
	deriveClose(closeRun)
	if err := s.persistLocked(); err != nil {
		s.state.Closes[closeID] = before
		s.state.Attempts = s.state.Attempts[:attemptCount]
		return nil, err
	}
	return cloneAttempt(attempt), nil
}

func (s *Store) FinishAttempt(attemptID string, result RunnerResult, runErr error) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	var attempt *Attempt
	for _, candidate := range s.state.Attempts {
		if candidate.ID == attemptID {
			attempt = candidate
			break
		}
	}
	if attempt == nil {
		return fmt.Errorf("attempt not found")
	}
	if attempt.FinishedAt != nil {
		return fmt.Errorf("finished attempt is immutable")
	}
	closeRun := s.state.Closes[attempt.CloseRunID]
	before := cloneClose(closeRun)
	beforeAttempt := cloneAttempt(attempt)
	step := closeRun.Steps[attempt.StepKey]
	now := time.Now().UTC()
	attempt.FinishedAt = &now
	attempt.Receipt = result.Receipt
	step.Receipt = result.Receipt
	step.ExternalRunID = result.ExternalRunID
	step.ObservedAt = &now
	if runErr != nil {
		attempt.Execution = ExecutionFailed
		attempt.Verification = VerificationFailed
		attempt.Error = runErr.Error()
		step.Execution = ExecutionFailed
		step.Verification = VerificationFailed
		step.Error = runErr.Error()
	} else {
		attempt.Execution = ExecutionSucceeded
		attempt.Verification = result.Verification
		if attempt.Verification == "" {
			attempt.Verification = VerificationNotChecked
		}
		step.Execution = ExecutionSucceeded
		step.Verification = attempt.Verification
		step.LastSucceededAt = &now
		if step.Verification == VerificationPassed {
			step.LastVerifiedAt = &now
		}
		step.Error = ""
	}
	closeRun.UpdatedAt = now
	deriveClose(closeRun)
	if err := s.persistLocked(); err != nil {
		s.state.Closes[attempt.CloseRunID] = before
		*attempt = *beforeAttempt
		return err
	}
	return nil
}

func (s *Store) Attempts(closeID string) []*Attempt {
	s.mu.RLock()
	defer s.mu.RUnlock()
	result := make([]*Attempt, 0)
	for _, attempt := range s.state.Attempts {
		if attempt.CloseRunID == closeID {
			result = append(result, cloneAttempt(attempt))
		}
	}
	sort.Slice(result, func(i, j int) bool { return result[i].StartedAt.After(result[j].StartedAt) })
	return result
}

func (s *Store) persistLocked() error {
	if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil {
		return fmt.Errorf("create control store directory: %w", err)
	}
	payload, err := json.MarshalIndent(s.state, "", "  ")
	if err != nil {
		return fmt.Errorf("encode control store: %w", err)
	}
	temporary := s.path + ".tmp"
	if err := os.WriteFile(temporary, payload, 0o600); err != nil {
		return fmt.Errorf("write control store: %w", err)
	}
	if err := os.Rename(temporary, s.path); err != nil {
		return fmt.Errorf("commit control store: %w", err)
	}
	return nil
}

func cloneClose(input *CloseRun) *CloseRun {
	payload, _ := json.Marshal(input)
	var result CloseRun
	_ = json.Unmarshal(payload, &result)
	return &result
}

func cloneAttempt(input *Attempt) *Attempt {
	payload, _ := json.Marshal(input)
	var result Attempt
	_ = json.Unmarshal(payload, &result)
	return &result
}
