package errors

import (
	"encoding/json"
	sysErrors "errors"
	"fmt"
	"strings"

	"github.com/spf13/cobra"
	"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
)

const (
	MISSING_PROJECT_ID = `the project ID is not currently set.
	
It can be set on the command level by re-running your command with the --project-id flag.
	
You can configure it for all commands by running:
	
  $ stackit config set --project-id xxx
	
or you can also set it through the environment variable [STACKIT_PROJECT_ID]`

	MISSING_REGION = `the region is not currently set.
	
It can be set on the command level by re-running your command with the --region flag.
	
You can configure it for all commands by running:
	
  $ stackit config set --region xxx
	
or you can also set it through the environment variable [STACKIT_REGION]`

	EMPTY_UPDATE = `please specify at least one field to update.
	
Get details on the available flags by re-running your command with the --help flag.`

	REQUIRED_MUTUALLY_EXCLUSIVE_FLAGS = `the following flags are mutually exclusive and at least one of them is required: %s`

	FAILED_AUTH = `you are not authenticated.

You can authenticate as a user by running:
  $ stackit auth login

or use a service account by running:
  $ stackit auth activate-service-account`

	SESSION_EXPIRED = `Session is expired. Please log in again first.
	
You can authenticate as a user by running:
$ stackit auth login

or use a service account by running:
$ stackit auth activate-service-account`

	ACCESS_TOKEN_EXPIRED = `Access token is expired. Please log in again first.
	
You can authenticate as a user by running:
$ stackit auth login

or use a service account by running:
$ stackit auth activate-service-account`

	FAILED_SERVICE_ACCOUNT_ACTIVATION = `could not setup authentication based on the provided service account credentials. 
Please double check if they are correctly configured.

For more details run:
  $ stackit auth activate-service-account -h`

	SET_INEXISTENT_PROFILE = `the configuration profile %[1]q you are trying to set doesn't exist.

To create it, run:
  $ stackit config profile create %[1]q`

	DELETE_INEXISTENT_PROFILE = `the configuration profile %q does not exist.

To list all profiles, run:
  $ stackit config profile list`

	DELETE_DEFAULT_PROFILE = `the default configuration profile %q cannot be deleted.`

	OBSERVABILITY_INVALID_INPUT_PLAN = `the instance plan was not correctly provided. 

Either provide the plan ID:
  $ %[1]s --plan-id <PLAN ID> [flags]

or provide plan name:
  $ %[1]s --plan-name <PLAN NAME> [flags]

For more details on the available plans, run:
  $ stackit %[2]s plans`

	OBSERVABILITY_INVALID_PLAN = `the provided instance plan is not valid.
	
  %s
  
  For more details on the available plans, run:
	$ stackit %s plans`

	DSA_INVALID_INPUT_PLAN = `the instance plan was not correctly provided. 

Either provide the plan ID:
  $ %[1]s --plan-id <PLAN ID> [flags]

or provide plan name and version:
  $ %[1]s --plan-name <PLAN NAME> --version <VERSION> [flags]

For more details on the available plans, run:
  $ stackit %[2]s plans`

	DSA_INVALID_PLAN = `the provided instance plan is not valid.
	
%s

For more details on the available plans, run:
  $ stackit %s plans`

	DATABASE_INVALID_INPUT_FLAVOR = `the instance flavor was not correctly provided. 

Either provide flavor ID by:
  $ %[1]s --flavor-id <FLAVOR ID> [flags]

or provide CPU and RAM:
  $ %[1]s --cpu <CPU> --ram <RAM> [flags]

For more details on the available flavors, run:
  $ stackit %[2]s options --flavors`

	DATABASE_INVALID_FLAVOR = `the provided instance flavor is not valid.
	
%s

For more details on the available flavors, run:
  $ stackit %s options --flavors`

	DATABASE_INVALID_STORAGE = `invalid instance storage.
	
%[1]s

For more details on the available storages for the configured flavor (%[3]s), run:
  $ stackit %[2]s options --storages --flavor-id %[3]s`

	FLAG_VALIDATION = `the provided flag --%s is invalid: %s`

	ARG_VALIDATION = `the provided argument "%s" is invalid: %s`

	ARG_UNKNOWN = `unknown argument %q`

	ARG_MISSING = `missing argument %q`

	SINGLE_ARG_EXPECTED = `expected 1 argument %q, %d were provided`

	SINGLE_OPTIONAL_ARG_EXPECTED = `expected no more than 1 argument %q, %d were provided`

	SUBCOMMAND_UNKNOWN = `unknown subcommand %q`

	SUBCOMMAND_MISSING = `missing subcommand`

	INVALID_PROFILE_NAME = `the profile name %q is invalid.
	
The profile name can only contain lowercase letters, numbers, and "-" and cannot be empty or "default". It can't start with a "-".`

	USAGE_TIP = `For usage help, run:
  $ %s --help`

	SERVICE_DISABLED = `This service isn't enabled for the current project.

To enable it, run:
  $ stackit %s enable`

	IAAS_SERVER_MISSING_VOLUME_SIZE = `The "boot-volume-size" flag must be provided when "boot-volume-source-type" is "image".`

	IAAS_SERVER_MISSING_VOLUME_ID = `The "boot-volume-source-id" flag must be provided together with "boot-volume-source-type" flag.`

	IAAS_SERVER_MISSING_VOLUME_TYPE = `The "boot-volume-source-type" flag must be provided together with "boot-volume-source-id" flag.`

	IAAS_SERVER_MISSING_IMAGE_OR_VOLUME_FLAGS = `Either "image-id" or "boot-volume-source-type" and "boot-volume-source-id" flags must be provided.`

	IAAS_SERVER_NIC_ATTACH_MISSING_NIC_ID = `The "network-interface-id" flag must be provided if the "create" flag is not provided.`

	IAAS_SERVER_NIC_DETACH_MISSING_NIC_ID = `The "network-interface-id" flag must be provided if the "delete" flag is not provided.`

	PROFILE_ALREADY_EXISTS = `profile %[1]q already exists. 

To delete it, run:
  $ stackit config profile delete %[1]s`

	PROFILE_DOES_NOT_EXIST = `The profile %q does not exist.

To list all profiles, run:
  $ stackit config profile list`

	FILE_ALREADY_EXISTS = `file %q already exists in the export path. Delete the existing file or define a different export path`

	FLAG_MUST_BE_PROVIDED_WHEN_ANOTHER_FLAG_IS_SET = `The flag %[1]q must be provided when %[2]q is set`

	MULTIPLE_FLAGS_MUST_BE_PROVIDED_WHEN_ANOTHER_FLAG_IS_SET = `The flags %[1]v must be provided when one of the flags %[2]v is set`

	ONE_OF_THE_FLAGS_MUST_BE_PROVIDED_WHEN_ANOTHER_FLAG_IS_SET = `One of the flags %[1]v must be provided when %[2]q is set`
)

type ServerNicAttachMissingNicIdError struct {
	Cmd *cobra.Command
}

func (e *ServerNicAttachMissingNicIdError) Error() string {
	return IAAS_SERVER_NIC_ATTACH_MISSING_NIC_ID
}

type ServerNicDetachMissingNicIdError struct {
	Cmd *cobra.Command
}

func (e *ServerNicDetachMissingNicIdError) Error() string {
	return IAAS_SERVER_NIC_DETACH_MISSING_NIC_ID
}

type ServerCreateMissingVolumeIdError struct {
	Cmd *cobra.Command
}

func (e *ServerCreateMissingVolumeIdError) Error() string {
	return IAAS_SERVER_MISSING_VOLUME_ID
}

type ServerCreateMissingVolumeTypeError struct {
	Cmd *cobra.Command
}

func (e *ServerCreateMissingVolumeTypeError) Error() string {
	return IAAS_SERVER_MISSING_VOLUME_TYPE
}

type ServerCreateMissingFlagsError struct {
	Cmd *cobra.Command
}

func (e *ServerCreateMissingFlagsError) Error() string {
	return IAAS_SERVER_MISSING_IMAGE_OR_VOLUME_FLAGS
}

type ServerCreateError struct {
	Cmd *cobra.Command
}

func (e *ServerCreateError) Error() string {
	return IAAS_SERVER_MISSING_VOLUME_SIZE
}

type ProjectIdError struct{}

func (e *ProjectIdError) Error() string {
	return MISSING_PROJECT_ID
}

type RegionError struct{}

func (e *RegionError) Error() string {
	return MISSING_REGION
}

type EmptyUpdateError struct{}

func (e *EmptyUpdateError) Error() string {
	return EMPTY_UPDATE
}

type AuthError struct{}

func (e *AuthError) Error() string {
	return FAILED_AUTH
}

type SessionExpiredError struct{}

func (e *SessionExpiredError) Error() string {
	return SESSION_EXPIRED
}

type AccessTokenExpiredError struct{}

func (e *AccessTokenExpiredError) Error() string {
	return ACCESS_TOKEN_EXPIRED
}

type ActivateServiceAccountError struct{}

func (e *ActivateServiceAccountError) Error() string {
	return FAILED_SERVICE_ACCOUNT_ACTIVATION
}

type SetInexistentProfile struct {
	Profile string
}

func (e *SetInexistentProfile) Error() string {
	return fmt.Sprintf(SET_INEXISTENT_PROFILE, e.Profile)
}

type DeleteInexistentProfile struct {
	Profile string
}

func (e *DeleteInexistentProfile) Error() string {
	return fmt.Sprintf(DELETE_INEXISTENT_PROFILE, e.Profile)
}

type DeleteDefaultProfile struct {
	DefaultProfile string
}

func (e *DeleteDefaultProfile) Error() string {
	return fmt.Sprintf(DELETE_DEFAULT_PROFILE, e.DefaultProfile)
}

type ObservabilityInputPlanError struct {
	Cmd  *cobra.Command
	Args []string
}

func (e *ObservabilityInputPlanError) Error() string {
	fullCommandPath := e.Cmd.CommandPath()
	if len(e.Args) > 0 {
		fullCommandPath = fmt.Sprintf("%s %s", fullCommandPath, strings.Join(e.Args, " "))
	}
	// Assumes a structure of the form "stackit <service> <resource> <operation>"
	service := e.Cmd.Parent().Parent().Use

	return fmt.Sprintf(OBSERVABILITY_INVALID_INPUT_PLAN, fullCommandPath, service)
}

type ObservabilityInvalidPlanError struct {
	Service string
	Details string
}

func (e *ObservabilityInvalidPlanError) Error() string {
	return fmt.Sprintf(OBSERVABILITY_INVALID_PLAN, e.Details, e.Service)
}

type DSAInputPlanError struct {
	Cmd  *cobra.Command
	Args []string
}

func (e *DSAInputPlanError) Error() string {
	fullCommandPath := e.Cmd.CommandPath()
	if len(e.Args) > 0 {
		fullCommandPath = fmt.Sprintf("%s %s", fullCommandPath, strings.Join(e.Args, " "))
	}
	// Assumes a structure of the form "stackit <service> <resource> <operation>"
	service := e.Cmd.Parent().Parent().Use

	return fmt.Sprintf(DSA_INVALID_INPUT_PLAN, fullCommandPath, service)
}

type DSAInvalidPlanError struct {
	Service string
	Details string
}

func (e *DSAInvalidPlanError) Error() string {
	return fmt.Sprintf(DSA_INVALID_PLAN, e.Details, e.Service)
}

type DatabaseInputFlavorError struct {
	Service string
	Cmd     *cobra.Command
	Args    []string
}

func (e *DatabaseInputFlavorError) Error() string {
	fullCommandPath := e.Cmd.CommandPath()
	if len(e.Args) > 0 {
		fullCommandPath = fmt.Sprintf("%s %s", fullCommandPath, strings.Join(e.Args, " "))
	}

	if e.Service == "" {
		// Assumes a structure of the form "stackit <service> <resource> <operation>"
		e.Service = e.Cmd.Parent().Parent().Use
	}

	return fmt.Sprintf(DATABASE_INVALID_INPUT_FLAVOR, fullCommandPath, e.Service)
}

type DatabaseInvalidFlavorError struct {
	Service string
	Details string
}

func (e *DatabaseInvalidFlavorError) Error() string {
	return fmt.Sprintf(DATABASE_INVALID_FLAVOR, e.Details, e.Service)
}

type DatabaseInvalidStorageError struct {
	Service  string
	Details  string
	FlavorId string
}

func (e *DatabaseInvalidStorageError) Error() string {
	return fmt.Sprintf(DATABASE_INVALID_STORAGE, e.Details, e.Service, e.FlavorId)
}

type FlagValidationError struct {
	Flag    string
	Details string
}

func (e *FlagValidationError) Error() string {
	return fmt.Sprintf(FLAG_VALIDATION, e.Flag, e.Details)
}

type RequiredMutuallyExclusiveFlagsError struct {
	Flags []string
}

func (e *RequiredMutuallyExclusiveFlagsError) Error() string {
	return fmt.Sprintf(REQUIRED_MUTUALLY_EXCLUSIVE_FLAGS, strings.Join(e.Flags, ", "))
}

type ArgValidationError struct {
	Arg     string
	Details string
}

func (e *ArgValidationError) Error() string {
	return fmt.Sprintf(ARG_VALIDATION, e.Arg, e.Details)
}

type SingleArgExpectedError struct {
	Cmd      *cobra.Command
	Expected string
	Count    int
}

func (e *SingleArgExpectedError) Error() string {
	var err error
	if e.Count > 1 {
		err = fmt.Errorf(SINGLE_ARG_EXPECTED, e.Expected, e.Count)
	} else {
		err = fmt.Errorf(ARG_MISSING, e.Expected)
	}
	return AppendUsageTip(err, e.Cmd).Error()
}

type SingleOptionalArgExpectedError struct {
	Cmd      *cobra.Command
	Expected string
	Count    int
}

func (e *SingleOptionalArgExpectedError) Error() string {
	err := fmt.Errorf(SINGLE_OPTIONAL_ARG_EXPECTED, e.Expected, e.Count)
	return AppendUsageTip(err, e.Cmd).Error()
}

// Used when an unexpected non-flag input (either arg or subcommand) is found
type InputUnknownError struct {
	ProvidedInput string
	Cmd           *cobra.Command
}

func (e *InputUnknownError) Error() string {
	// To decide whether the unexpected input is an arg or a subcommand, we assume that only leaf commands (ie, don't have subcomamnds) take args
	var err error
	if !e.Cmd.HasSubCommands() {
		err = fmt.Errorf(ARG_UNKNOWN, e.ProvidedInput)
	} else {
		err = fmt.Errorf(SUBCOMMAND_UNKNOWN, e.ProvidedInput)
	}
	return AppendUsageTip(err, e.Cmd).Error()
}

type SubcommandMissingError struct {
	Cmd *cobra.Command
}

func (e *SubcommandMissingError) Error() string {
	err := fmt.Errorf("%s", SUBCOMMAND_MISSING)
	return AppendUsageTip(err, e.Cmd).Error()
}

// Returns a wrapped error whose message adds a tip on how to check out --help for the command
func AppendUsageTip(err error, cmd *cobra.Command) error {
	tip := fmt.Sprintf(USAGE_TIP, cmd.CommandPath())
	return fmt.Errorf("%w.\n\n%s", err, tip)
}

type InvalidProfileNameError struct {
	Profile string
}

func (e *InvalidProfileNameError) Error() string {
	return fmt.Sprintf(INVALID_PROFILE_NAME, e.Profile)
}

type ServiceDisabledError struct {
	Service string
}

func (e *ServiceDisabledError) Error() string {
	return fmt.Sprintf(SERVICE_DISABLED, e.Service)
}

type ProfileAlreadyExistsError struct {
	Profile string
}

func (e *ProfileAlreadyExistsError) Error() string {
	return fmt.Sprintf(PROFILE_ALREADY_EXISTS, e.Profile)
}

type ProfileDoesNotExistError struct {
	Profile string
}

func (e *ProfileDoesNotExistError) Error() string {
	return fmt.Sprintf(PROFILE_DOES_NOT_EXIST, e.Profile)
}

type FileAlreadyExistsError struct {
	Filename string
}

func (e *FileAlreadyExistsError) Error() string { return fmt.Sprintf(FILE_ALREADY_EXISTS, e.Filename) }

type DependingFlagIsMissing struct {
	MissingFlag string
	SetFlag     string
}

func (e *DependingFlagIsMissing) Error() string {
	return fmt.Sprintf(FLAG_MUST_BE_PROVIDED_WHEN_ANOTHER_FLAG_IS_SET, fmt.Sprintf("--%s", e.MissingFlag), fmt.Sprintf("--%s", e.SetFlag))
}

type MultipleFlagsAreMissing struct {
	MissingFlags []string
	SetFlags     []string
}

func (e *MultipleFlagsAreMissing) Error() string {
	return fmt.Sprintf(MULTIPLE_FLAGS_MUST_BE_PROVIDED_WHEN_ANOTHER_FLAG_IS_SET, e.MissingFlags, e.SetFlags)
}

type OneOfFlagsIsMissing struct {
	MissingFlags []string
	SetFlag      string
}

func (e *OneOfFlagsIsMissing) Error() string {
	return fmt.Sprintf(ONE_OF_THE_FLAGS_MUST_BE_PROVIDED_WHEN_ANOTHER_FLAG_IS_SET, e.MissingFlags, e.SetFlag)
}

// ___FORMATTING_ERRORS_________________________________________________________

// InvalidFormatError indicates that an unsupported format was provided.
type InvalidFormatError struct {
	Format string // The invalid format that was provided
}

func (e *InvalidFormatError) Error() string {
	if e.Format != "" {
		return fmt.Sprintf("unsupported format provided: %s", e.Format)
	}
	return "unsupported format provided"
}

// NewInvalidFormatError creates a new InvalidFormatError with the provided format.
func NewInvalidFormatError(format string) *InvalidFormatError {
	return &InvalidFormatError{
		Format: format,
	}
}

// ___BUILD_REQUEST_ERRORS______________________________________________________
// BuildRequestError indicates that a request could not be built.
type BuildRequestError struct {
	Reason string // Optional: specific reason why the request failed to build
	Err    error  // Optional: underlying error
}

func (e *BuildRequestError) Error() string {
	if e.Reason != "" && e.Err != nil {
		return fmt.Sprintf("could not build request (%s): %v", e.Reason, e.Err)
	}
	if e.Reason != "" {
		return fmt.Sprintf("could not build request: %s", e.Reason)
	}
	if e.Err != nil {
		return fmt.Sprintf("could not build request: %v", e.Err)
	}
	return "could not build request"
}

func (e *BuildRequestError) Unwrap() error {
	return e.Err
}

// NewBuildRequestError creates a new BuildRequestError with optional reason and underlying error.
func NewBuildRequestError(reason string, err error) *BuildRequestError {
	return &BuildRequestError{
		Reason: reason,
		Err:    err,
	}
}

// ___REQUESTS_ERRORS___________________________________________________________
// RequestFailedError indicates that an API request failed.
// If the provided error is an OpenAPI error, the status code and message from the error body will be included in the error message.
type RequestFailedError struct {
	Err error // Optional: underlying error
}

func (e *RequestFailedError) Error() string {
	var msg = "request failed"

	if e.Err != nil {
		var oApiErr *oapierror.GenericOpenAPIError
		if sysErrors.As(e.Err, &oApiErr) {
			// Extract status code from OpenAPI error header if it exists
			if oApiErr.StatusCode > 0 {
				msg += fmt.Sprintf(" (%d)", oApiErr.StatusCode)
			}

			// Try to extract message from OpenAPI error body
			if bodyMsg := extractOpenApiMessageFromBody(oApiErr.Body); bodyMsg != "" {
				msg += fmt.Sprintf(": %s", bodyMsg)
			} else if trimmedBody := strings.TrimSpace(string(oApiErr.Body)); trimmedBody != "" {
				msg += fmt.Sprintf(": %s", trimmedBody)
			} else {
				// Otherwise use the Go error
				msg += fmt.Sprintf(": %v", e.Err)
			}
		} else {
			// If this can't be cased into a OpenApi error use the Go error
			msg += fmt.Sprintf(": %v", e.Err)
		}
	}

	return msg
}

func (e *RequestFailedError) Unwrap() error {
	return e.Err
}

// NewRequestFailedError creates a new RequestFailedError with optional details.
func NewRequestFailedError(err error) *RequestFailedError {
	return &RequestFailedError{
		Err: err,
	}
}

// ___HELPERS___________________________________________________________________
// extractOpenApiMessageFromBody attempts to parse a JSON body and extract the "message"
// field. It returns an empty string if parsing fails or if no message is found.
func extractOpenApiMessageFromBody(body []byte) string {
	trimmedBody := strings.TrimSpace(string(body))
	// Return early if empty.
	if trimmedBody == "" {
		return ""
	}

	// Try to unmarshal as a structured error first
	var errorBody struct {
		Message string `json:"message"`
	}
	if err := json.Unmarshal(body, &errorBody); err == nil && errorBody.Message != "" {
		if msg := strings.TrimSpace(errorBody.Message); msg != "" {
			return msg
		}
	}

	// If that fails, try to unmarshal as a plain string
	var plainBody string
	if err := json.Unmarshal(body, &plainBody); err == nil && plainBody != "" {
		if msg := strings.TrimSpace(plainBody); msg != "" {
			return msg
		}
		return ""
	}

	// All parsing attempts failed or yielded no message
	return ""
}
