package models

import (
	"errors"
	"fmt"
	"maps"
	"time"

	"github.com/bacalhau-project/bacalhau/pkg/bacerrors"
)

const (
	DetailsKeyIsError        = "IsError"
	DetailsKeyHint           = "Hint"
	DetailsKeyRetryable      = "Retryable"
	DetailsKeyFailsExecution = "FailsExecution"
	DetailsKeyNewState       = "NewState"
	DetailsKeyErrorCode      = "ErrorCode"
)

type HasHint interface {
	// Hint A human-readable string that advises the user on how they might solve the error.
	Hint() string
}

type HasRetryable interface {
	// Retryable Whether the error could be retried, assuming the same input and
	// node configuration; i.e. the error is transient and due to network
	// capacity or service outage.
	//
	// If a component raises an error with Retryable() as true, the system may
	// retry the last action after some length of time. If it is false, it
	// should not try the action again, and may choose an alternative action or
	// fail the job.
	Retryable() bool
}

type HasFailsExecution interface {
	// FailsExecution Whether this error means that the associated execution cannot
	// continue.
	//
	// If a component raises an error with FailsExecution() as true,
	// the hosting executor should report the execution as failed and stop any
	// further steps.
	FailsExecution() bool
}

type HasDetails interface {
	// Details An extra set of metadata provided by the error.
	Details() map[string]string
}

// EventTopic is a high level categorisation that can be applied to an event. It
// should be a human-readable string with no dynamic content. They are used to
// disambiguate events from the same component occurring in different contexts.
// For example, an event emitted by S3 storage used as an input source and the
// same event emitted by S3 storage used as a publisher would be tagged with
// different topics.
//
// EventTopics do not need to conform to a centralized list – each module should
// use event topics that make sense for their own logic. Event topics SHOULD be
// unique.
type EventTopic string

// Event represents a progress report made by the system in its attempt to run a
// job. Events are generated by the orchestrator and also passed back to the
// orchestrator from the compute node.
//
// Events may be delivered in an async fashion – i.e, they may arrive much later
// than the moment they occurred.
type Event struct {
	// A human-readable string giving the user all the information they need to
	// understand and respond to an Event, if a response is required.
	Message string `json:"Message"`

	// The topic of the event. See the documentation on EventTopic.
	Topic EventTopic `json:"Topic"`

	// The moment the event occurred, which may be different to the moment it
	// was recorded.
	Timestamp time.Time `json:"Timestamp"`

	// Any additional metadata that the system or user may need to know about
	// the event in order to handle it properly.
	Details map[string]string `json:"Details,omitempty"`
}

// NewEvent returns a new Event with the given topic.
func NewEvent(topic EventTopic) *Event {
	return &Event{
		Topic:     topic,
		Timestamp: time.Now(),
		Details:   make(map[string]string),
	}
}

// WithMessage returns a new Event with the given message and topic.
func (e *Event) WithMessage(message string) *Event {
	e.Message = message
	return e
}

// WithError returns a new Event with the given error.
func (e *Event) WithError(err error) *Event {
	e.Message = err.Error()
	return e.WithDetail(DetailsKeyIsError, "true")
}

// WithHint returns a new Event with the given hint.
func (e *Event) WithHint(hint string) *Event {
	if hint != "" {
		return e.WithDetail(DetailsKeyHint, hint)
	}
	return e
}

// WithRetryable returns a new Event with the given retryable flag.
func (e *Event) WithRetryable(retryable bool) *Event {
	if retryable {
		return e.WithDetail(DetailsKeyRetryable, "true")
	}
	return e
}

// WithFailsExecution returns a new Event with the given fails execution flag.
func (e *Event) WithFailsExecution(failsExecution bool) *Event {
	if failsExecution {
		return e.WithDetail(DetailsKeyFailsExecution, "true")
	}
	return e
}

// WithErrorCode returns a new Event with the given error code.
func (e *Event) WithErrorCode(errorCode string) *Event {
	if errorCode != "" {
		return e.WithDetail(DetailsKeyErrorCode, errorCode)
	}
	return e
}

// WithDetails returns a new Event with the given details and topic.
func (e *Event) WithDetails(details map[string]string) *Event {
	maps.Copy(e.Details, details)
	return e
}

// WithDetail returns a new Event with the given detail and topic.
func (e *Event) WithDetail(key, value string) *Event {
	e.Details[key] = value
	return e
}

// HasError returns true if the event is an error.
func (e *Event) HasError() bool {
	return e.Details[DetailsKeyIsError] == "true"
}

// HasStateUpdate returns true if the event is a state update.
func (e *Event) HasStateUpdate() bool {
	_, ok := e.Details[DetailsKeyNewState]
	return ok
}

// GetJobStateIfPresent returns the job state from the event, if it is a state update.
func (e *Event) GetJobStateIfPresent() (JobStateType, error) {
	if !e.HasStateUpdate() {
		return JobStateTypeUndefined, nil
	}
	var jobState JobStateType
	if err := jobState.UnmarshalText([]byte(e.Details[DetailsKeyNewState])); err != nil {
		return JobStateTypeUndefined, err
	}
	return jobState, nil
}

// EventFromError converts an error into an Event tagged with the passed event
// topic.
//
// This method allows errors to implement extra interfaces (above) to do
// "attribute-based error reporting". The design principle is that errors can
// report a set of extra flags that have well defined semantics which the system
// can then respond to with specific behavior. This allows introducing or
// refactoring error types without higher-level components needing to be
// modified – they simply continue to respond to the presence of attributes.
//
// This is instead of the system having a centralized set of known error types
// and programming in specific behavior in response to them, which is brittle
// and requires updating all of the error responses when the types change.
func EventFromError(topic EventTopic, err error) *Event {
	event := NewEvent(topic).WithError(err)

	// if error is bacerrors
	var bacErr bacerrors.Error
	if errors.As(err, &bacErr) {
		event = event.
			WithDetails(bacErr.Details()).
			WithHint(bacErr.Hint()).
			WithRetryable(bacErr.Retryable()).
			WithFailsExecution(bacErr.FailsExecution()).
			WithErrorCode(fmt.Sprintf("%s:%s", bacErr.Component(), bacErr.Code()))
	} else {
		if hasDetails, ok := err.(HasDetails); ok {
			event = event.WithDetails(hasDetails.Details())
		}
		if hasHint, ok := err.(HasHint); ok {
			event = event.WithHint(hasHint.Hint())
		}
		if hasRetryable, ok := err.(HasRetryable); ok {
			event = event.WithRetryable(hasRetryable.Retryable())
		}
		if hasFailsExecution, ok := err.(HasFailsExecution); ok {
			event = event.WithFailsExecution(hasFailsExecution.FailsExecution())
		}
	}
	return event
}
