package ddp

import (
	"crypto/sha256"
	"encoding/hex"
	"io"
	"strings"
	"time"
)

// ----------------------------------------------------------------------
// EJSON document interface
// ----------------------------------------------------------------------
// https://github.com/meteor/meteor/blob/devel/packages/ddp/DDP.md#appendix-ejson

// Doc provides hides the complexity of ejson documents.
type Doc struct {
	root interface{}
}

// NewDoc creates a new document from a generic json parsed document.
func NewDoc(in interface{}) *Doc {
	doc := &Doc{in}
	return doc
}

// Map locates a map[string]interface{} - json object - at a path
// or returns nil if not found.
func (d *Doc) Map(path string) map[string]interface{} {
	item := d.Item(path)
	if item != nil {
		switch m := item.(type) {
		case map[string]interface{}:
			return m
		default:
			return nil
		}
	}
	return nil
}

// Array locates an []interface{} - json array - at a path
// or returns nil if not found.
func (d *Doc) Array(path string) []interface{} {
	item := d.Item(path)
	if item != nil {
		switch m := item.(type) {
		case []interface{}:
			return m
		default:
			return nil
		}
	}
	return nil
}

// StringArray locates an []string - json array of strings - at a path
// or returns nil if not found. The string array will contain all string values
// in the array and skip any non-string entries.
func (d *Doc) StringArray(path string) []string {
	item := d.Item(path)
	if item != nil {
		switch m := item.(type) {
		case []interface{}:
			items := []string{}
			for _, item := range m {
				switch val := item.(type) {
				case string:
					items = append(items, val)
				}
			}
			return items
		case []string:
			return m
		default:
			return nil
		}
	}
	return nil
}

// String returns a string value located at the path or an empty string if not found.
func (d *Doc) String(path string) string {
	item := d.Item(path)
	if item != nil {
		switch m := item.(type) {
		case string:
			return m
		default:
			return ""
		}
	}
	return ""
}

// Bool returns a boolean value located at the path or false if not found.
func (d *Doc) Bool(path string) bool {
	item := d.Item(path)
	if item != nil {
		switch m := item.(type) {
		case bool:
			return m
		default:
			return false
		}
	}
	return false
}

// Float returns a float64 value located at the path or zero if not found.
func (d *Doc) Float(path string) float64 {
	item := d.Item(path)
	if item != nil {
		switch m := item.(type) {
		case float64:
			return m
		default:
			return 0
		}
	}
	return 0
}

// Time returns a time value located at the path or nil if not found.
func (d *Doc) Time(path string) time.Time {
	ticks := d.Float(path + ".$date")
	var t time.Time
	if ticks > 0 {
		sec := int64(ticks / 1000)
		t = time.Unix(int64(sec), 0)
	}
	return t
}

// Item locates a "raw" item at the provided path, returning
// the item found or nil if not found.
func (d *Doc) Item(path string) interface{} {
	item := d.root
	steps := strings.Split(path, ".")
	for _, step := range steps {
		// This is an intermediate step - we must be in a map
		switch m := item.(type) {
		case map[string]interface{}:
			item = m[step]
		case Update:
			item = m[step]
		default:
			return nil
		}
	}
	return item
}

// Set a value for a path. Intermediate items are created as necessary.
func (d *Doc) Set(path string, value interface{}) {
	item := d.root
	steps := strings.Split(path, ".")
	last := steps[len(steps)-1]
	steps = steps[:len(steps)-1]
	for _, step := range steps {
		// This is an intermediate step - we must be in a map
		switch m := item.(type) {
		case map[string]interface{}:
			item = m[step]
			if item == nil {
				item = map[string]interface{}{}
				m[step] = item
			}
		default:
			return
		}
	}
	// Item is now the last map so we just set the value
	switch m := item.(type) {
	case map[string]interface{}:
		m[last] = value
	}
}

// Accounts password login support
type Login struct {
	User     *User     `json:"user"`
	Password *Password `json:"password"`
}

func NewEmailLogin(email, pass string) *Login {
	return &Login{User: &User{Email: email}, Password: NewPassword(pass)}
}

func NewUsernameLogin(user, pass string) *Login {
	return &Login{User: &User{Username: user}, Password: NewPassword(pass)}
}

type LoginResume struct {
	Token string `json:"resume"`
}

func NewLoginResume(token string) *LoginResume {
	return &LoginResume{Token: token}
}

type User struct {
	Email    string `json:"email,omitempty"`
	Username string `json:"username,omitempty"`
}

type Password struct {
	Digest    string `json:"digest"`
	Algorithm string `json:"algorithm"`
}

func NewPassword(pass string) *Password {
	sha := sha256.New()
	io.WriteString(sha, pass)
	digest := sha.Sum(nil)
	return &Password{Digest: hex.EncodeToString(digest), Algorithm: "sha-256"}
}