package object

import (
	"errors"
	"fmt"
	"math"
	"servitor/ansi"
	"servitor/gemtext"
	"servitor/hypertext"
	"servitor/markdown"
	"servitor/mime"
	"servitor/plaintext"
	"net/url"
	"time"
)

type Object map[string]any

var (
	ErrKeyNotPresent = errors.New("key is not present")
	ErrKeyWrongType  = errors.New("value is incorrect type")
)

/* Go doesn't allow generic methods */
func getPrimitive[T any](o Object, key string) (T, error) {
	var zero T
	if value, ok := o[key]; !ok || value == nil {
		return zero, fmt.Errorf("failed to extract \"%s\": %w", key, ErrKeyNotPresent)
	} else if narrowed, ok := value.(T); !ok {
		return zero, fmt.Errorf("failed to extract \"%s\": %w: is %T", key, ErrKeyWrongType, value)
	} else {
		return narrowed, nil
	}
}

func (o Object) GetAny(key string) (any, error) {
	return getPrimitive[any](o, key)
}

func (o Object) GetString(key string) (string, error) {
	value, err := getPrimitive[string](o, key)
	if err != nil {
		return "", err
	}
	value = ansi.Scrub(value)
	if value == "" {
		return "", ErrKeyNotPresent
	}
	return value, nil
}

func (o Object) GetNumber(key string) (uint64, error) {
	if number, err := getPrimitive[float64](o, key); err != nil {
		return 0, err
	} else if number != math.Trunc(number) {
		return 0, fmt.Errorf("failed to extract \"%s\": value is not a non-integer number", key)
	} else {
		return uint64(number), nil
	}
}

func (o Object) GetObject(key string) (Object, error) {
	return getPrimitive[map[string]any](o, key)
}

func (o Object) GetList(key string) ([]any, error) {
	if value, err := o.GetAny(key); err != nil {
		return nil, err
	} else if asList, isList := value.([]any); isList {
		return asList, nil
	} else {
		return []any{value}, nil
	}
}

func (o Object) GetTime(key string) (time.Time, error) {
	if value, err := o.GetString(key); err != nil {
		return time.Time{}, err
	} else {
		timestamp, err := time.Parse(time.RFC3339, value)
		if err != nil {
			return time.Time{}, fmt.Errorf("failed to parse time \"%s\": %w", key, err)
		}
		return timestamp, nil
	}
}

func (o Object) GetURL(key string) (*url.URL, error) {
	if value, err := o.GetString(key); err != nil {
		return nil, err
	} else {
		address, err := url.Parse(value)
		if err != nil {
			return nil, fmt.Errorf("failed to parse URL \"%s\": %w", key, err)
		}
		return address, nil
	}
}

func (o Object) GetMediaType(key string) (*mime.MediaType, error) {
	if value, err := o.GetString(key); err != nil {
		return nil, err
	} else {
		mediaType, err := mime.Parse(value)
		if err != nil {
			return nil, fmt.Errorf("failed to parse mime type \"%s\": %w", key, err)
		}
		return mediaType, nil
	}
}

type Markup interface {
	Render(width int) string
}

func (o Object) GetMarkup(contentKey string, mediaTypeKey string) (Markup, []string, error) {
	content, err := o.GetString(contentKey)
	if err != nil {
		return nil, nil, err
	}
	mediaType, err := o.GetMediaType(mediaTypeKey)
	if errors.Is(err, ErrKeyNotPresent) {
		mediaType = mime.Default()
	} else if err != nil {
		return nil, nil, err
	}

	switch mediaType.Essence {
	case "text/plain":
		return plaintext.NewMarkup(content)
	case "text/html":
		return hypertext.NewMarkup(content)
	case "text/gemini":
		return gemtext.NewMarkup(content)
	case "text/markdown":
		return markdown.NewMarkup(content)
	default:
		return nil, nil, errors.New("cannot render text of mime type " + mediaType.Essence)
	}
}