package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"strconv"
	"strings"
	"time"

	"github.com/nicholas-fedor/shoutrrr/pkg/format"
	"github.com/nicholas-fedor/shoutrrr/pkg/types"
)

// urlTypeName is the reflected type name for *url.URL fields,
// used to identify services that require a webhook URL.
const urlTypeName = "*url.URL"

// errFieldNotFound is returned when a config field does not exist.
var errFieldNotFound = errors.New("field not found")

// errFieldNotSettable is returned when a config field cannot be set.
var errFieldNotSettable = errors.New("field not settable")

// errInvalidBoolValue is returned when a bool field receives an invalid value.
var errInvalidBoolValue = errors.New("invalid bool value")

// errInvalidUintValue is returned when a uint field receives an unparseable value.
var errInvalidUintValue = errors.New("invalid uint value")

// errUnsupportedFieldType is returned when a field type is unsupported for string assignment.
var errUnsupportedFieldType = errors.New("unsupported field type for string assignment")

// classifyType returns a human-readable type string for typ.
// If isEnum is true, returns "enum" regardless of the underlying kind.
// time.Duration is classified as "string" to allow duration values like "10s".
//
//nolint:exhaustive // Only handling common config field types.
func classifyType(typ reflect.Type, isEnum bool) string {
	if isEnum {
		return "enum"
	}

	// Handle time.Duration as string to allow "10s", "5m", etc.
	if typ == reflect.TypeFor[time.Duration]() {
		return "string"
	}

	switch typ.Kind() {
	case reflect.String:
		return "string"
	case reflect.Bool:
		return "bool"
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return "int"
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		return "uint"
	case reflect.Slice, reflect.Array:
		return "array"
	case reflect.Map:
		return "map"
	default:
		return typ.Kind().String()
	}
}

// urlPartToString converts a slice of URLPart values to a comma-separated
// string representation (e.g., "user,password" for URLUser and URLPassword).
func urlPartToString(parts []format.URLPart) string {
	if len(parts) == 0 {
		return ""
	}

	names := make([]string, len(parts))
	for i, part := range parts {
		switch part {
		case format.URLUser:
			names[i] = "user"
		case format.URLPassword:
			names[i] = "password"
		case format.URLHost:
			names[i] = "host"
		case format.URLPort:
			names[i] = "port"
		case format.URLPath:
			names[i] = "path"
		case format.URLQuery:
			names[i] = "query"
		default:
			if part > format.URLPath {
				offset := int(part - format.URLPath)
				names[i] = fmt.Sprintf("path%d", offset)
			} else {
				names[i] = fmt.Sprintf("unknown(%d)", part)
			}
		}
	}

	return strings.Join(names, ",")
}

// getEnumNames returns the string names from ef, or nil if ef is nil.
func getEnumNames(ef types.EnumFormatter) []string {
	if ef == nil {
		return nil
	}

	return ef.Names()
}

// setFieldFromString sets configValue[fieldName] to value using reflection.
// Supports string, bool, and unsigned integer field types. Bool values are
// parsed via format.ParseBool(). Unsigned integers are parsed via
// strconv.ParseUint using the field's bit size. Returns an error if the field
// is invalid, cannot be set, or the value cannot be parsed for the field type.
//
//nolint:exhaustive // uint kinds handled; all others fallback to default.
func setFieldFromString(configValue reflect.Value, fieldName, value string) error {
	field := configValue.FieldByName(fieldName)
	if !field.IsValid() {
		return fmt.Errorf("%w: %q", errFieldNotFound, fieldName)
	}

	if !field.CanSet() {
		return fmt.Errorf("%w: %q", errFieldNotSettable, fieldName)
	}

	switch field.Kind() {
	case reflect.String:
		field.SetString(value)

		return nil
	case reflect.Bool:
		bv, ok := format.ParseBool(value, false)
		if !ok {
			return fmt.Errorf("%w: %q for field %q", errInvalidBoolValue, value, fieldName)
		}

		field.SetBool(bv)

		return nil
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		bits := field.Type().Bits()

		parsed, err := strconv.ParseUint(value, 10, bits)
		if err != nil {
			return fmt.Errorf("%w: %q for field %q: %w", errInvalidUintValue, value, fieldName, err)
		}

		field.SetUint(parsed)

		return nil
	default:
		return fmt.Errorf("%w: %s for field %q", errUnsupportedFieldType, field.Kind(), fieldName)
	}
}

// marshalError serializes err as a JSON errorResult.
//
//nolint:errcheck,errchkjson // errorResult struct always marshals successfully.
func marshalError(err error) string {
	if err == nil {
		return marshalErrorStr("unknown error")
	}

	data, _ := json.Marshal(errorResult{Error: err.Error()})

	return string(data)
}

// marshalErrorStr serializes msg as a JSON errorResult.
//
//nolint:errcheck,errchkjson // errorResult struct always marshals.
func marshalErrorStr(msg string) string {
	data, _ := json.Marshal(errorResult{Error: msg})

	return string(data)
}

// isNillableKind returns true if the reflect.Kind represents a type that
// supports nil values. Calling IsNil() on a non-nillable kind panics.
//
//nolint:exhaustive // Using default case for non-nillable kinds.
func isNillableKind(k reflect.Kind) bool {
	switch k {
	case reflect.Pointer, reflect.Interface, reflect.Slice, reflect.Map,
		reflect.Func, reflect.Chan:
		return true
	default:
		return false
	}
}
