//nolint:gocritic // Lifecycle orchestration deliberately passes immutable activation contracts by value.
package byoc

import (
	"context"
	"errors"
	"fmt"
	"os"
	"strings"
	"syscall"
	"time"
)

// ActivatePrepared runs local activation preparation after the immutable
// journal boundary is validated and while holding the same lifecycle lock as
// activation and destroy. Preparation must not mutate external cloud state.
//
//nolint:gocritic // The immutable lifecycle contract is deliberately copied across the orchestration boundary.
func ActivatePrepared(
	ctx context.Context,
	input Input,
	driver Driver,
	prepare func(context.Context) error,
) (*Journal, error) {
	if driver == nil {
		return nil, errors.New("BYOC activation driver is required")
	}
	fingerprint, err := Fingerprint(input)
	if err != nil {
		return nil, err
	}
	release, err := acquireLifecycleLock(input.StateDir)
	if err != nil {
		return nil, err
	}
	defer release()

	journal, err := LoadJournal(input.StateDir)
	switch {
	case err == nil:
		if journal.State == StateDestroyed {
			return nil, errors.New("BYOC activation is terminally destroyed; finish cp9 destroy and create a new session")
		}
		if journal.Fingerprint != fingerprint {
			return nil, fmt.Errorf(
				"BYOC activation fingerprint changed (%s -> %s); run cp9 destroy before changing immutable BYOC inputs",
				journal.Fingerprint,
				fingerprint,
			)
		}
	case errors.Is(err, os.ErrNotExist):
		journal = nil
	default:
		return nil, err
	}
	if prepare != nil {
		if err := prepare(ctx); err != nil {
			return journal, err
		}
	}

	preflight, err := driver.Preflight(ctx, input, journal)
	if err != nil {
		return journal, fmt.Errorf("BYOC read-only preflight: %w", err)
	}
	if preflight == nil {
		return journal, errors.New("BYOC read-only preflight returned no evidence")
	}
	preflight.CompletedAt = time.Now().UTC()

	if journal == nil {
		now := time.Now().UTC()
		journal = &Journal{
			APIVersion:  JournalAPIVersion,
			Kind:        JournalKind,
			Session:     input.Session,
			Fingerprint: fingerprint,
			Provenance:  activationProvenance(input),
			State:       StateActivating,
			Stages:      make(map[Stage]*StageRecord, len(ActivationStages)),
			CreatedAt:   now,
			UpdatedAt:   now,
		}
		for _, stage := range ActivationStages {
			journal.Stages[stage] = &StageRecord{State: StagePending}
		}
	}
	if journal.Provenance == nil {
		journal.Provenance = activationProvenance(input)
	}
	journal.Preflight = preflight
	journal.State = StateActivating
	if err := saveJournal(input.StateDir, journal); err != nil {
		return nil, err
	}

	for _, stage := range ActivationStages {
		record := journal.Stages[stage]
		if record == nil {
			record = &StageRecord{State: StagePending}
			journal.Stages[stage] = record
		}
		if record.State == StageDone {
			result, revalidateErr := driver.Revalidate(ctx, input, stage, record)
			if revalidateErr == nil {
				if result != nil && result.Artifacts != nil {
					record.Artifacts = cloneArtifacts(result.Artifacts)
				}
				continue
			}
			if !errors.Is(revalidateErr, ErrNeedsReconcile) {
				return journal, fmt.Errorf("revalidate BYOC stage %s: %w", stage, revalidateErr)
			}
		}

		now := time.Now().UTC()
		record.State = StageRunning
		record.Attempts++
		record.StartedAt = &now
		record.CompletedAt = nil
		record.Message = ""
		journal.State = StateActivating
		if err := saveJournal(input.StateDir, journal); err != nil {
			return journal, err
		}

		result, applyErr := driver.Apply(ctx, input, stage, journal)
		completed := time.Now().UTC()
		record.CompletedAt = &completed
		if applyErr != nil {
			record.State = StageFailed
			record.Message = fmt.Sprintf("%s failed; rerun cp9 byoc activate to resume", stage)
			journal.State = StateFailed
			if saveErr := saveJournal(input.StateDir, journal); saveErr != nil {
				return journal, errors.Join(fmt.Errorf("apply BYOC stage %s: %w", stage, applyErr), saveErr)
			}
			return journal, fmt.Errorf("apply BYOC stage %s: %w", stage, applyErr)
		}
		record.State = StageDone
		record.Message = ""
		if result != nil {
			record.Artifacts = cloneArtifacts(result.Artifacts)
		}
		if err := saveJournal(input.StateDir, journal); err != nil {
			return journal, err
		}
	}

	journal.State = StateReady
	if err := saveJournal(input.StateDir, journal); err != nil {
		return journal, err
	}
	return journal, nil
}

// ObserveActivation live-revalidates completed activation stages without
// running preflight, Apply, or writing the journal. It is the read-only system
// boundary used by `cp9 byoc status`.
//
//nolint:gocritic // The immutable lifecycle contract is deliberately copied across the observation boundary.
func ObserveActivation(ctx context.Context, input Input, driver Driver, journal *Journal) (*ActivationObservation, error) {
	if driver == nil || journal == nil {
		return nil, errors.New("BYOC activation observation requires a driver and journal")
	}
	fingerprint, err := Fingerprint(input)
	if err != nil {
		return nil, err
	}
	if journal.Session != input.Session || journal.Fingerprint != fingerprint {
		return nil, errors.New("BYOC activation observation input does not match the bound journal")
	}

	observation := &ActivationObservation{
		Ready:  true,
		Stages: make(map[Stage]StageObservation, len(ActivationStages)),
	}
	for _, stage := range ActivationStages {
		record := journal.Stages[stage]
		if record == nil || record.State == StagePending {
			observation.Ready = false
			observation.Stages[stage] = StageObservation{State: ObservedPending}
			continue
		}
		switch record.State {
		case StageRunning:
			observation.Ready = false
			observation.Stages[stage] = StageObservation{State: ObservedInterrupted, Message: "stage was left running; rerun activation to reconcile"}
		case StageFailed:
			observation.Ready = false
			observation.Stages[stage] = StageObservation{State: ObservedFailed, Message: record.Message}
		case StageDone:
			_, revalidateErr := driver.Revalidate(ctx, input, stage, record)
			switch {
			case revalidateErr == nil:
				observation.Stages[stage] = StageObservation{State: ObservedReady}
			case errors.Is(revalidateErr, ErrNeedsReconcile):
				observation.Ready = false
				observation.Stages[stage] = StageObservation{
					State:   ObservedNeedsReconcile,
					Message: "journal is complete but live state needs reconciliation",
				}
			default:
				observation.Ready = false
				observation.Stages[stage] = StageObservation{State: ObservedObservationError, Message: revalidateErr.Error()}
			}
		default:
			observation.Ready = false
			observation.Stages[stage] = StageObservation{State: ObservedObservationError, Message: "journal contains an unsupported stage state"}
		}
	}
	return observation, nil
}

func cloneArtifacts(values map[string]string) map[string]string {
	if len(values) == 0 {
		return nil
	}
	cloned := make(map[string]string, len(values))
	for key, value := range values {
		key = strings.TrimSpace(key)
		if key == "" {
			continue
		}
		cloned[key] = strings.TrimSpace(value)
	}
	return cloned
}

func acquireLifecycleLock(sessionStateDir string) (func(), error) {
	if err := os.MkdirAll(StateDir(sessionStateDir), 0o700); err != nil {
		return nil, fmt.Errorf("create BYOC state directory for lock: %w", err)
	}
	lockFile, err := os.OpenFile(LockPath(sessionStateDir), os.O_CREATE|os.O_RDWR, 0o600)
	if err != nil {
		return nil, fmt.Errorf("open BYOC lifecycle lock: %w", err)
	}
	if err := syscall.Flock(int(lockFile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
		_ = lockFile.Close()
		return nil, fmt.Errorf("BYOC lifecycle operation is already running for %q", sessionStateDir)
	}
	return func() {
		_ = syscall.Flock(int(lockFile.Fd()), syscall.LOCK_UN)
		_ = lockFile.Close()
	}, nil
}
