package core

import (
	"bytes"
	"encoding/json"
	"io"
	"strconv"
)

type Resource interface {
	GetContent(writer io.Writer) error
	GetSubResource(name string) (Resource, error)
}

// Archivable abstracts the archive operation of testbed resource(e.g. collecting & uploading logs and metrics)
type Archivable interface {
	Archive() (urlMap map[string]string, err error) // map[artifact-identifier]url
}

const (
	MetaSubResource = "!meta"
	FlagFileType    = "!filetype"
	FlagRead        = "!read"
	FlagWrite       = "!write"
	ContentEncoding = "contentEncoding"
)

type MetaResource struct {
	metas map[string]string
}

func (m MetaResource) GetContent(writer io.Writer) error {
	edr := json.NewEncoder(writer)
	return edr.Encode(m.metas)
}

func (m MetaResource) GetSubResource(name string) (Resource, error) {
	if name == MetaSubResource {
		return MetaResource{metas: map[string]string{FlagFileType: "f", ContentEncoding: "meta", FlagRead: "1"}}, nil
	}
	c, ok := m.metas[name]
	if !ok {
		return nil, NotFound
	}
	return NewStringFile(c), nil
}

func GetMeta(resource Resource, name string) (string, error) {
	meta, err := resource.GetSubResource(MetaSubResource)
	if err != nil {
		return "", err
	}
	r, err := meta.GetSubResource(name)
	if err != nil {
		return "", err
	}
	buf := &bytes.Buffer{}
	err = r.GetContent(buf)
	if err != nil {
		return "", err
	}
	return buf.String(), nil
}

func GetContent(resource Resource) ([]byte, error) {
	buffer := &bytes.Buffer{}
	err := resource.GetContent(buffer)
	if err != nil {
		return nil, err
	}
	return buffer.Bytes(), nil
}

func IsDir(resource Resource) (bool, error) {
	t, e := GetMeta(resource, FlagFileType)
	return t == "d", e
}
func IsFile(resource Resource) (bool, error) {
	t, e := GetMeta(resource, FlagFileType)
	return t == "f", e
}

type PathRecorder struct {
	parent Resource
	name   string
}

func (p PathRecorder) GetContent(_ io.Writer) error {
	return NotImplementedError
}

func (p PathRecorder) GetSubResource(name string) (Resource, error) {
	t := PathRecorder{name: name, parent: p}
	return t, nil
}

type DirEntry struct {
	Name         string                   `json:"name"`
	Resource     Resource                 `json:"-"`
	ResourceFunc func() (Resource, error) `json:"-"`
}

type DirInterface interface {
	ListEntry() ([]DirEntry, error)
}

type Directory struct {
	impl DirInterface
}

func (d *Directory) Init(dir DirInterface) {
	d.impl = dir
}

type DirectoryContent struct {
	FileType    string     `json:"fileType"`
	Subresource []DirEntry `json:"subResources"`
}

func (d Directory) GetContent(writer io.Writer) error {
	e := json.NewEncoder(writer)
	entries, err := d.impl.ListEntry()
	if err != nil {
		return err
	}
	if entries == nil {
		entries = make([]DirEntry, 0)
	}
	c := DirectoryContent{
		FileType:    "directory",
		Subresource: entries,
	}
	return e.Encode(c)
}

func (d Directory) GetSubResource(name string) (Resource, error) {
	if name == MetaSubResource {
		return MetaResource{metas: map[string]string{FlagFileType: "d", FlagRead: "1"}}, nil
	}
	entries, err := d.impl.ListEntry()
	if err != nil {
		return nil, err
	}
	for _, entry := range entries {
		if entry.Name == name {
			if entry.ResourceFunc != nil {
				return entry.ResourceFunc()
			}
			return entry.Resource, nil
		}
	}
	return nil, NotFound
}

type StringFile struct {
	content string
}

func NewStringFile(s string) StringFile {
	return StringFile{content: s}
}

func (s StringFile) GetContent(writer io.Writer) error {
	l, err := io.WriteString(writer, s.content)
	if err != nil {
		return err
	}
	if l != len(s.content) {
		return NewInternalError("bytes buffer writer full")
	}
	return err
}

func (s StringFile) GetSubResource(name string) (Resource, error) {
	if name == MetaSubResource {
		return MetaResource{metas: map[string]string{FlagFileType: "f", ContentEncoding: "string", FlagRead: "1"}}, nil
	}
	return nil, NotFound
}

type JSON struct {
	obj interface{}
}

func NewJSON(obj interface{}) JSON {
	return JSON{
		obj: obj,
	}
}

func (j JSON) GetContent(writer io.Writer) error {
	d := json.NewEncoder(writer)
	return d.Encode(j.obj)
}

func (j JSON) GetSubResource(name string) (Resource, error) {
	if name == MetaSubResource {
		return MetaResource{metas: map[string]string{FlagFileType: "f", ContentEncoding: "json", FlagRead: "1"}}, nil
	}
	return jsonSubResource(j.obj, name)
}

func jsonSubResource(obj interface{}, name string) (Resource, error) {
	switch t := obj.(type) {
	case map[string]interface{}:
		{
			sub, ok := t[name]
			if !ok {
				return nil, NotFound
			}
			return NewJSON(sub), nil
		}
	case []interface{}:
		i, err := strconv.Atoi(name)
		if err != nil {
			return nil, NotFound
		}
		sub := t[i]
		return NewJSON(sub), nil
	case string:
		if name == "toString" { // todo do we need toString or auto convert?
			return NewStringFile(t), nil
		}
		return nil, NotFound
	default:
		marshal, err := json.Marshal(t)
		if err != nil {
			return nil, err
		}
		tmp := map[string]interface{}{}
		err = json.Unmarshal(marshal, &tmp)
		if err != nil {
			return nil, NotFound
		}
		return jsonSubResource(tmp, name)
	}
}
