package flowserver

import (
	"encoding/json"
	"errors"
	"flow"
	"flow/flowserver/flowmsg"
	"flow/registry"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"sync"
	"time"

	"github.com/gorilla/websocket"
)

const (
	storePath = "store"
)

// NodeActivity when nodes are processing
type NodeActivity struct {
	Status    string    `json:"status"` // nodeStatus, Running, error, result
	StartTime time.Time `json:"startTime"`
	EndTime   time.Time `json:"endTime"`
	Data      flow.Data `json:"data"`
	Error     string    `json:"error"`
}

// FlowSession Create a session and link clients
type FlowSession struct {
	sync.Mutex
	mgr *FlowSessionManager

	ID string // Random handle for sessionID
	// List of clients on this session
	clients []*websocket.Conn
	Chat    ChatRoom

	RawDoc       []byte // Just share data
	nodeActivity map[string]*NodeActivity

	flow *flow.Flow
}

//NewSession creates and initializes a NewSession
func NewSession(mgr *FlowSessionManager, ID string) *FlowSession {
	// Or load
	//
	//
	fpath, err := pathFor(ID)
	if err != nil {
		log.Println("Error fetching filepath", err)
	}

	rawDoc, err := ioutil.ReadFile(fpath)
	if err != nil {
		log.Println("Warning: unable to read file:", err)
	}
	if rawDoc == nil {
		rawDoc = []byte{}
	}

	s := &FlowSession{
		Mutex:        sync.Mutex{},
		mgr:          mgr,
		ID:           ID,
		clients:      []*websocket.Conn{},
		Chat:         ChatRoom{},
		RawDoc:       rawDoc,
		nodeActivity: map[string]*NodeActivity{},

		flow: nil,
	}
	return s
}

// ClientAdd add a client to session
func (s *FlowSession) ClientAdd(c *websocket.Conn) error {
	s.Lock()
	defer s.Unlock()
	err := c.WriteJSON(flowmsg.SendMessage{OP: "sessionJoin", ID: s.ID})
	if err != nil {
		return err
	}
	desc, err := registry.Descriptions()
	if err != nil {
		return err
	}
	err = c.WriteJSON(flowmsg.SendMessage{OP: "registry", Data: desc})
	if err != nil {
		return err
	}
	s.clients = append(s.clients, c)

	if len(s.RawDoc) == 0 {
		return nil
	}
	err = c.WriteJSON(flowmsg.SendMessage{OP: "document", Data: json.RawMessage(s.RawDoc)})
	if err != nil {
		return err
	}

	// Sending activity
	return c.WriteJSON(flowmsg.SendMessage{OP: "nodeActivity", Data: s.nodeActivity})

	// Send registry
}

// ClientRemove remove client from Session
func (s *FlowSession) ClientRemove(c *websocket.Conn) {
	s.Lock()
	defer s.Unlock()
	for i, cl := range s.clients {
		if cl == c {
			s.clients = append(s.clients[:i], s.clients[i+1:]...)
			break
		}
	}
	s.Chat.ClientRemove(c)
	/*if len(s.clients) == 0 && s.flow == nil {
		log.Println("No more clients, remove session")
		delete(s.mgr.sessions, s.ID) // Clear memory session
	}*/
}

// ChatJoin the chat room on this session
func (s *FlowSession) ChatJoin(c *websocket.Conn, handle string) {
	s.Chat.ClientAdd(c, handle)
}

// DocumentUpdate client c Updates the session document
func (s *FlowSession) DocumentUpdate(c *websocket.Conn, data []byte) error {
	s.Lock()
	defer s.Unlock()

	s.RawDoc = make([]byte, len(data))
	copy(s.RawDoc, data)

	return s.broadcast(c, flowmsg.SendMessage{OP: "document", Data: json.RawMessage(s.RawDoc)})
}

// DocumentSave persist document in a file
func (s *FlowSession) DocumentSave() error {
	s.Lock()
	defer s.Unlock()

	fpath, err := pathFor(s.ID)
	if err != nil {
		log.Println("path error", err)
		return err
	}

	err = ioutil.WriteFile(fpath, s.RawDoc, os.FileMode(0600))
	if err != nil {
		log.Println("writing file", err)
		return err
	}

	return s.broadcast(nil, flowmsg.SendMessage{OP: "documentSave", Data: "saved"})
}

// Document send document to client c
func (s *FlowSession) Document(c *websocket.Conn) error {
	s.Lock()
	defer s.Unlock()

	return c.WriteJSON(flowmsg.SendMessage{OP: "document", Data: json.RawMessage(s.RawDoc)})
}

// NodeRun a node triggering results
// Build a flow and run
func (s *FlowSession) NodeRun(c *websocket.Conn, data []byte) error {
	var err error
	ID := string(data[1 : len(data)-1]) // remove " instead of unmarshalling json
	if s.flow != nil {
		return errors.New("node already running")
	}

	// Clear activity
	s.nodeActivity = map[string]*NodeActivity{}
	s.Broadcast(nil, flowmsg.SendMessage{OP: "nodeActivity", Data: s.nodeActivity})

	go func() {
		log.Printf("Building flow from '%s'\n", string(s.RawDoc))

		localr := registry.Global.Clone()
		//Add our log func that is not in global registry
		localr.Register("Log", s.Log)

		s.flow, err = FlowBuild(s.RawDoc, localr)

		if err != nil {
			log.Println("Flow error:", err)
			return
		}
		defer func() { // After routing gone
			s.flow = nil
		}()
		s.flow.Hook(flow.Hook{
			Any: func(name string, ID string, triggerTime time.Time, extra ...flow.Data) {
				s.Lock()
				defer s.Unlock()

				act, ok := s.nodeActivity[ID]
				if !ok {
					act = &NodeActivity{}
					s.nodeActivity[ID] = act
				}
				status := ""
				switch name {
				case "Wait":
					status = "waiting"
				case "Start":
					status = "running"
					act.StartTime = triggerTime
				case "Finish":
					status = "finish"
					act.EndTime = triggerTime
					act.Data = extra[0]
				case "Error":
					status = "error"
					act.EndTime = triggerTime
					act.Error = fmt.Sprint(extra[0])
				}
				if act.Status == status {
					return
				}
				act.Status = status
				s.broadcast(nil, flowmsg.SendMessage{OP: "nodeActivity", Data: s.nodeActivity})

			},
		})

		op := s.flow.Res(ID)
		if s.flow.Err() != nil {
			log.Println("error fetching document", s.flow.Err())
		}
		op.Process()
		if s.flow.Err() != nil {
			log.Println("error processing node", s.flow.Err())
		}
		/*func() {
			s.Lock()
			defer s.Unlock()
			if len(s.clients) == 0 {
				log.Println("No more clients, remove session")
				delete(s.mgr.sessions, s.ID) // Clear memory session
			}
		}()*/

	}()
	return nil
}

// Log broadcast a log event
func (s *FlowSession) Log(v interface{}) (interface{}, error) {
	err := s.Broadcast(nil, flowmsg.SendMessage{OP: "sessionLog", Data: v})
	if err != nil {
		return nil, err
	}
	return v, nil
}

// Broadcast broadcast a message in session besides C
func (s *FlowSession) Broadcast(c *websocket.Conn, v interface{}) error {
	s.Lock()
	defer s.Unlock()

	return s.broadcast(c, v)
}

//
func (s *FlowSession) broadcast(c *websocket.Conn, v interface{}) error {
	for _, sc := range s.clients {
		if sc == c { // ours
			continue
		}
		err := sc.WriteJSON(v)
		if err != nil {
			return err
		}
	}
	return nil

}

func pathFor(ID string) (string, error) {
	{
		err := os.MkdirAll(storePath, 0755)
		if err != nil {
			return "", err
		}
	}
	fpath := filepath.Clean(ID)
	_, fpath = filepath.Split(fpath)
	fpath = filepath.Join(storePath, fpath)
	return fpath, nil
}
