package workspace

import (
	"crypto/rand"
	"encoding/hex"
	"sync/atomic"

	"go.uber.org/zap"

	iop "iop/proto/gen/iop"
)

const workspaceObservationLogKey = "node_workspace_observation"

type workspaceObservationEvent string

const (
	workspaceObservationTool    workspaceObservationEvent = "tool"
	workspaceObservationCleanup workspaceObservationEvent = "cleanup"
)

// workspaceObservation is deliberately raw-free. It carries no request id,
// workspace ref, path, command id, environment, content, stdout, stderr, or
// error text. Correlation is generated at Open and is not derived from any
// caller-controlled identity.
type workspaceObservation struct {
	event         workspaceObservationEvent
	correlation   string
	operation     string
	outcome       string
	errorCode     string
	durationMS    int64
	truncated     bool
	processCount  int32
	artifactCount int32
}

type workspaceObserver interface {
	Emit(workspaceObservation) error
}

type workspaceNoopObserver struct{}

func (workspaceNoopObserver) Emit(workspaceObservation) error { return nil }

type workspaceSafeObserver struct {
	inner    workspaceObserver
	failures atomic.Int64
}

func (o *workspaceSafeObserver) Emit(observation workspaceObservation) error {
	if o == nil || o.inner == nil {
		return nil
	}
	defer func() {
		if recover() != nil {
			o.failures.Add(1)
		}
	}()
	if err := o.inner.Emit(observation); err != nil {
		o.failures.Add(1)
	}
	return nil
}

func (o *workspaceSafeObserver) failureCount() int64 {
	if o == nil {
		return 0
	}
	return o.failures.Load()
}

type zapWorkspaceObserver struct {
	logger *zap.Logger
}

func newZapWorkspaceObserver(logger *zap.Logger) workspaceObserver {
	if logger == nil {
		return workspaceNoopObserver{}
	}
	return &zapWorkspaceObserver{logger: logger}
}

func (o *zapWorkspaceObserver) Emit(observation workspaceObservation) error {
	if o == nil || o.logger == nil {
		return nil
	}
	o.logger.Info(workspaceObservationLogKey,
		zap.String("correlation", observation.correlation),
		zap.String("event", string(observation.event)),
		zap.String("operation", observation.operation),
		zap.String("outcome", observation.outcome),
		zap.String("error_code", observation.errorCode),
		zap.Int64("duration_ms", observation.durationMS),
		zap.Bool("truncated", observation.truncated),
		zap.Int32("process_count", observation.processCount),
		zap.Int32("artifact_count", observation.artifactCount),
	)
	return nil
}

var workspaceCorrelationFallback atomic.Uint64

func newWorkspaceCorrelation() string {
	var value [12]byte
	if _, err := rand.Read(value[:]); err == nil {
		return "ws-" + hex.EncodeToString(value[:])
	}
	return "ws-fallback-" + formatWorkspaceFallback(workspaceCorrelationFallback.Add(1))
}

func formatWorkspaceFallback(value uint64) string {
	const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
	if value == 0 {
		return "0"
	}
	var encoded [13]byte
	index := len(encoded)
	for value > 0 {
		index--
		encoded[index] = alphabet[value%36]
		value /= 36
	}
	return string(encoded[index:])
}

func workspaceOperationName(operation iop.WorkspaceOperation) string {
	switch operation {
	case iop.WorkspaceOperation_WORKSPACE_OPERATION_READ:
		return "read"
	case iop.WorkspaceOperation_WORKSPACE_OPERATION_LIST:
		return "list"
	case iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE:
		return "write"
	case iop.WorkspaceOperation_WORKSPACE_OPERATION_DELETE:
		return "delete"
	case iop.WorkspaceOperation_WORKSPACE_OPERATION_COMMAND:
		return "command"
	default:
		return "unknown"
	}
}

func workspaceOutcome(status iop.WorkspaceStatus) string {
	switch status {
	case iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS:
		return "success"
	case iop.WorkspaceStatus_WORKSPACE_STATUS_TIMEOUT:
		return "timeout"
	case iop.WorkspaceStatus_WORKSPACE_STATUS_CANCELLED:
		return "cancelled"
	case iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED:
		return "unsupported"
	case iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR:
		return "error"
	default:
		return "unknown"
	}
}

func workspaceErrorCode(code iop.WorkspaceErrorCode) string {
	switch code {
	case iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_UNSPECIFIED:
		return "none"
	case iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY:
		return "not_ready"
	case iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_UNSUPPORTED:
		return "unsupported"
	case iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST:
		return "invalid_request"
	case iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND:
		return "not_found"
	case iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_TIMEOUT:
		return "timeout"
	case iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_CANCELLED:
		return "cancelled"
	case iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INTERNAL:
		return "internal"
	default:
		return "unknown"
	}
}

func (r *Runtime) observeTool(correlation string, operation iop.WorkspaceOperation, result Result) {
	if r == nil || r.observer == nil {
		return
	}
	r.observer.Emit(workspaceObservation{
		event: workspaceObservationTool, correlation: correlation,
		operation: workspaceOperationName(operation), outcome: workspaceOutcome(result.Status),
		errorCode: workspaceErrorCode(result.Code), durationMS: result.DurationMS, truncated: result.Truncated,
	})
}

func (r *Runtime) observeCleanup(req *Request, result CleanupResult, durationMS int64) {
	if r == nil || r.observer == nil {
		return
	}
	correlation := ""
	if req != nil {
		correlation = req.correlation
	}
	r.observer.Emit(workspaceObservation{
		event: workspaceObservationCleanup, correlation: correlation, operation: "cleanup",
		outcome: workspaceOutcome(result.Status), errorCode: workspaceErrorCode(result.Code), durationMS: durationMS,
		processCount: result.CleanedProcesses, artifactCount: result.CleanedArtifacts,
	})
}
