// Package domain contains domain-specific error types for the EdgeConnect client
package domain

import (
	"errors"
	"fmt"
	"strings"
)

// ErrorCode represents different types of domain errors
type ErrorCode int

const (
	// Resource errors
	ErrResourceNotFound ErrorCode = iota
	ErrResourceAlreadyExists
	ErrResourceConflict

	// Validation errors
	ErrValidationFailed
	ErrInvalidConfiguration
	ErrInvalidInput

	// Business logic errors
	ErrQuotaExceeded
	ErrInsufficientPermissions
	ErrOperationNotAllowed

	// Infrastructure errors
	ErrNetworkError
	ErrAuthenticationFailed
	ErrServiceUnavailable
	ErrTimeout

	// Internal errors
	ErrInternalError
	ErrUnknownError
)

// String returns a human-readable string representation of the error code
func (e ErrorCode) String() string {
	switch e {
	case ErrResourceNotFound:
		return "RESOURCE_NOT_FOUND"
	case ErrResourceAlreadyExists:
		return "RESOURCE_ALREADY_EXISTS"
	case ErrResourceConflict:
		return "RESOURCE_CONFLICT"
	case ErrValidationFailed:
		return "VALIDATION_FAILED"
	case ErrInvalidConfiguration:
		return "INVALID_CONFIGURATION"
	case ErrInvalidInput:
		return "INVALID_INPUT"
	case ErrQuotaExceeded:
		return "QUOTA_EXCEEDED"
	case ErrInsufficientPermissions:
		return "INSUFFICIENT_PERMISSIONS"
	case ErrOperationNotAllowed:
		return "OPERATION_NOT_ALLOWED"
	case ErrNetworkError:
		return "NETWORK_ERROR"
	case ErrAuthenticationFailed:
		return "AUTHENTICATION_FAILED"
	case ErrServiceUnavailable:
		return "SERVICE_UNAVAILABLE"
	case ErrTimeout:
		return "TIMEOUT"
	case ErrInternalError:
		return "INTERNAL_ERROR"
	case ErrUnknownError:
		return "UNKNOWN_ERROR"
	default:
		return "UNDEFINED_ERROR"
	}
}

// DomainError represents a domain-specific error with detailed context
type DomainError struct {
	Code       ErrorCode            `json:"code"`
	Message    string               `json:"message"`
	Details    string               `json:"details,omitempty"`
	Cause      error                `json:"-"`
	Context    map[string]interface{} `json:"context,omitempty"`
	Resource   *ResourceIdentifier   `json:"resource,omitempty"`
	Operation  string               `json:"operation,omitempty"`
	Retryable  bool                 `json:"retryable"`
}

// ResourceIdentifier provides context about the resource involved in the error
type ResourceIdentifier struct {
	Type         string `json:"type"`
	Name         string `json:"name"`
	Organization string `json:"organization,omitempty"`
	Region       string `json:"region,omitempty"`
	Version      string `json:"version,omitempty"`
}

// Error implements the error interface
func (e *DomainError) Error() string {
	var parts []string

	if e.Operation != "" {
		parts = append(parts, fmt.Sprintf("operation %s failed", e.Operation))
	}

	if e.Resource != nil {
		parts = append(parts, fmt.Sprintf("resource %s", e.resourceString()))
	}

	parts = append(parts, e.Message)

	if e.Details != "" {
		parts = append(parts, e.Details)
	}

	result := strings.Join(parts, ": ")

	if e.Cause != nil {
		result = fmt.Sprintf("%s (caused by: %v)", result, e.Cause)
	}

	return result
}

// Unwrap returns the underlying cause for error wrapping
func (e *DomainError) Unwrap() error {
	return e.Cause
}

// Is checks if the error matches a specific error code
func (e *DomainError) Is(target error) bool {
	if de, ok := target.(*DomainError); ok {
		return e.Code == de.Code
	}
	return false
}

// IsRetryable indicates whether the operation should be retried
func (e *DomainError) IsRetryable() bool {
	return e.Retryable
}

// WithContext adds context information to the error
func (e *DomainError) WithContext(key string, value interface{}) *DomainError {
	if e.Context == nil {
		e.Context = make(map[string]interface{})
	}
	e.Context[key] = value
	return e
}

// WithDetails adds additional details to the error
func (e *DomainError) WithDetails(details string) *DomainError {
	e.Details = details
	return e
}

func (e *DomainError) resourceString() string {
	if e.Resource == nil {
		return ""
	}

	parts := []string{e.Resource.Type}

	if e.Resource.Organization != "" && e.Resource.Name != "" {
		parts = append(parts, fmt.Sprintf("%s/%s", e.Resource.Organization, e.Resource.Name))
	} else if e.Resource.Name != "" {
		parts = append(parts, e.Resource.Name)
	}

	if e.Resource.Version != "" {
		parts = append(parts, fmt.Sprintf("version %s", e.Resource.Version))
	}

	if e.Resource.Region != "" {
		parts = append(parts, fmt.Sprintf("in region %s", e.Resource.Region))
	}

	return strings.Join(parts, " ")
}

// Error creation helpers

// NewDomainError creates a new domain error with the specified code and message
func NewDomainError(code ErrorCode, message string) *DomainError {
	return &DomainError{
		Code:      code,
		Message:   message,
		Retryable: isRetryableByDefault(code),
	}
}

// NewDomainErrorWithCause creates a new domain error with an underlying cause
func NewDomainErrorWithCause(code ErrorCode, message string, cause error) *DomainError {
	return &DomainError{
		Code:      code,
		Message:   message,
		Cause:     cause,
		Retryable: isRetryableByDefault(code),
	}
}

// NewResourceError creates a domain error for resource-related operations
func NewResourceError(code ErrorCode, operation string, resource *ResourceIdentifier, message string) *DomainError {
	return &DomainError{
		Code:      code,
		Message:   message,
		Operation: operation,
		Resource:  resource,
		Retryable: isRetryableByDefault(code),
	}
}

func isRetryableByDefault(code ErrorCode) bool {
	switch code {
	case ErrNetworkError, ErrServiceUnavailable, ErrTimeout, ErrInternalError:
		return true
	default:
		return false
	}
}

// Predefined errors for common scenarios

var (
	// Resource errors
	ErrAppNotFound      = NewDomainError(ErrResourceNotFound, "application not found")
	ErrAppExists        = NewDomainError(ErrResourceAlreadyExists, "application already exists")
	ErrInstanceNotFound = NewDomainError(ErrResourceNotFound, "app instance not found")
	ErrInstanceExists   = NewDomainError(ErrResourceAlreadyExists, "app instance already exists")
	ErrCloudletNotFound = NewDomainError(ErrResourceNotFound, "cloudlet not found")

	// Validation errors
	ErrInvalidAppKey      = NewDomainError(ErrValidationFailed, "invalid application key")
	ErrInvalidInstanceKey = NewDomainError(ErrValidationFailed, "invalid app instance key")
	ErrInvalidCloudletKey = NewDomainError(ErrValidationFailed, "invalid cloudlet key")
	ErrMissingRegion      = NewDomainError(ErrValidationFailed, "region is required")

	// Business logic errors
	ErrDeploymentFailed  = NewDomainError(ErrOperationNotAllowed, "deployment failed")
	ErrRollbackFailed    = NewDomainError(ErrOperationNotAllowed, "rollback failed")
	ErrPlanningFailed    = NewDomainError(ErrOperationNotAllowed, "deployment planning failed")
)

// Helper functions for creating specific error scenarios

// NewAppError creates an error related to application operations
func NewAppError(code ErrorCode, operation string, appKey AppKey, region string, message string) *DomainError {
	resource := &ResourceIdentifier{
		Type:         "app",
		Organization: appKey.Organization,
		Name:         appKey.Name,
		Version:      appKey.Version,
		Region:       region,
	}
	return NewResourceError(code, operation, resource, message)
}

// NewInstanceError creates an error related to app instance operations
func NewInstanceError(code ErrorCode, operation string, instanceKey AppInstanceKey, region string, message string) *DomainError {
	resource := &ResourceIdentifier{
		Type:         "app-instance",
		Organization: instanceKey.Organization,
		Name:         instanceKey.Name,
		Region:       region,
	}
	return NewResourceError(code, operation, resource, message)
}

// NewCloudletError creates an error related to cloudlet operations
func NewCloudletError(code ErrorCode, operation string, cloudletKey CloudletKey, region string, message string) *DomainError {
	resource := &ResourceIdentifier{
		Type:         "cloudlet",
		Organization: cloudletKey.Organization,
		Name:         cloudletKey.Name,
		Region:       region,
	}
	return NewResourceError(code, operation, resource, message)
}

// Error checking utilities

// IsNotFoundError checks if an error indicates a resource was not found
func IsNotFoundError(err error) bool {
	var de *DomainError
	return errors.As(err, &de) && de.Code == ErrResourceNotFound
}

// IsValidationError checks if an error is a validation error
func IsValidationError(err error) bool {
	var de *DomainError
	return errors.As(err, &de) && (de.Code == ErrValidationFailed || de.Code == ErrInvalidInput || de.Code == ErrInvalidConfiguration)
}

// IsRetryableError checks if an error is retryable
func IsRetryableError(err error) bool {
	var de *DomainError
	if errors.As(err, &de) {
		return de.IsRetryable()
	}
	return false
}

// IsAuthenticationError checks if an error is authentication-related
func IsAuthenticationError(err error) bool {
	var de *DomainError
	return errors.As(err, &de) && de.Code == ErrAuthenticationFailed
}