package trpcd

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"strings"
	"sync"
	"time"

	"code.ivysaur.me/trpcd/DataSource"
)

const (
	TRPC_SESSION_HEADER = "X-Transmission-Session-Id"
)

type trpcRequest struct {
	Arguments map[string]interface{} `json:"arguments"`
	Method    string                 `json:"method"`
	Tag       int                    `json:"tag"`
}

type trpcResponse struct {
	Arguments interface{} `json:"arguments"`
	Result    string      `json:"result"`
	Tag       int         `json:"tag"`
}

func ErrorReponse(tag int, message string) trpcResponse {
	return trpcResponse{
		Arguments: map[string]interface{}{
			"message": message,
		},
		Result: "failure",
		Tag:    tag,
	}
}

type SessionInfo struct {
	data             DataSource.DataSession
	remoteAddr       string
	lastRequestTime  int64
	lastFullTransmit int64
}

type Server struct {
	prov DataSource.DataProvider

	pruneAfter time.Duration
	baseDir    string

	sessionsMtx sync.RWMutex
	sessions    map[string]*SessionInfo
}

func NewServer(prov DataSource.DataProvider, pruneAfterSeconds int, baseDir string) (*Server, error) {
	if !strings.HasSuffix(baseDir, "/") || !strings.HasPrefix(baseDir, "/") {
		return nil, errors.New("Base directory must start and end with trailing slash")
	}

	svr := &Server{
		prov:       prov,
		pruneAfter: time.Duration(pruneAfterSeconds) * time.Second,
		baseDir:    baseDir,
		sessions:   make(map[string]*SessionInfo),
	}
	go svr.PruneWorker()

	return svr, nil
}

func (this *Server) PruneWorker() {
	for {
		time.Sleep(this.pruneAfter)
		this.PruneNow()
	}
}

func (this *Server) PruneNow() {
	this.sessionsMtx.Lock()
	defer this.sessionsMtx.Unlock()

	cutOff := time.Now().Unix() - int64(this.pruneAfter.Seconds())

	for k, v := range this.sessions {
		if v.lastRequestTime < cutOff {
			log.Printf("Closing defunct connection [%s]", k[0:6])
			delete(this.sessions, k)
		}
	}
}

func (this *Server) NewSessionRequest(w http.ResponseWriter, r *http.Request) {
	identifierBytes := make([]byte, 32)
	n, err := rand.Read(identifierBytes)
	if err != nil {
		panic(err)
	}
	if n != len(identifierBytes) {
		panic(errors.New("Fatal randomness error"))
	}

	sessionId := base64.StdEncoding.EncodeToString(identifierBytes)

	w.Header().Set(TRPC_SESSION_HEADER, sessionId)

	this.sessionsMtx.Lock()
	this.sessions[sessionId] = &SessionInfo{
		data:            this.prov.NewSession(),
		remoteAddr:      r.RemoteAddr,
		lastRequestTime: time.Now().Unix(),
	}
	this.sessionsMtx.Unlock()

	log.Printf("New connection [%s] for IP %s", sessionId[0:6], r.RemoteAddr)

	w.WriteHeader(409)
	w.Write([]byte(`409: Invalid Session ID provided, check headers`))
}

func (this *Server) HandleError(w http.ResponseWriter, r *http.Request, code int, err error) {
	log.Printf("Error: [%s] %s", r.RemoteAddr, err.Error())

	w.WriteHeader(code)
	w.Write([]byte(fmt.Sprintf(`%d Error processing request`, code)))
}

func (this *Server) HandleRPCRequest(w http.ResponseWriter, r *http.Request) {
	sessionId := r.Header.Get(TRPC_SESSION_HEADER)
	if len(sessionId) == 0 {
		this.NewSessionRequest(w, r)
		return
	}

	this.sessionsMtx.RLock()
	session, ok := this.sessions[sessionId]
	this.sessionsMtx.RUnlock()
	if !ok {
		this.NewSessionRequest(w, r)
		return
	}

	// Bump lastrequest for session ID
	session.lastRequestTime = time.Now().Unix()

	// Read body, parse as JSON
	requestBody, err := ioutil.ReadAll(r.Body)
	if err != nil {
		this.HandleError(w, r, 500, err)
		return
	}

	rpcReq := trpcRequest{}
	err = json.Unmarshal(requestBody, &rpcReq)
	if err != nil {
		this.HandleError(w, r, 500, err)
		return
	}

	rpcResp := this.HandleRPCRequestMethod(rpcReq, session)

	responseBytes, err := json.Marshal(rpcResp)
	if err != nil {
		this.HandleError(w, r, 500, err)
		return
	}

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.WriteHeader(200)
	w.Write(responseBytes)
}

func (this *Server) HandleRPCRequestMethod(req trpcRequest, sinfo *SessionInfo) trpcResponse {

	session := sinfo.data

	switch req.Method {

	case "session-get":
		sinf, err := session.Session()
		if err != nil {
			return ErrorReponse(req.Tag, err.Error())
		}

		return trpcResponse{
			Arguments: sinf,
			Result:    "success",
			Tag:       req.Tag,
		}

	case "session-stats":
		sstats, err := session.SessionStats()
		if err != nil {
			return ErrorReponse(req.Tag, err.Error())
		}

		return trpcResponse{
			Arguments: sstats,
			Result:    "success",
			Tag:       req.Tag,
		}

	case "free-space":
		selectedPath, ok := req.Arguments["path"].(string)
		if !ok {
			return ErrorReponse(req.Tag, "Missing string 'path' in arguments")
		}

		freeSpace, err := session.FreeSpace(selectedPath)
		if err != nil {
			return ErrorReponse(req.Tag, err.Error())
		}

		return trpcResponse{
			Arguments: map[string]interface{}{
				"path":       selectedPath,
				"size-bytes": float64(freeSpace),
			},
			Result: "success",
			Tag:    req.Tag,
		}

	case "torrent-add":
		addResp, err := session.Add(req.Arguments)
		if err != nil {
			return ErrorReponse(req.Tag, err.Error())
		}

		keyName := "torrent-added"
		if addResp.DidAlreadyExist {
			keyName = "torrent-duplicate"
		}

		return trpcResponse{
			Arguments: map[string]DataSource.TorrentAddResponse{
				keyName: addResp,
			},
			Result: "success",
			Tag:    req.Tag,
		}

	case "torrent-get":
		selection, err := DataSource.NewTorrentSelection(&req.Arguments)
		if err != nil {
			return ErrorReponse(req.Tag, err.Error())
		}

		updateState := session.MonotonicUpdateCounter()

		if selection.Mode == DataSource.TSMODE_RECENTLY_ACTIVE && updateState == sinfo.lastFullTransmit {
			// no change, incremental request, fast-path out of here
			return trpcResponse{
				Arguments: map[string][]int{
					"removed":  make([]int, 0),
					"torrents": make([]int, 0),
				},
				Result: "success",
				Tag:    req.Tag,
			}
		}
		sinfo.lastFullTransmit = updateState

		allTorrents, err := session.Get()
		if err != nil {
			return ErrorReponse(req.Tag, err.Error())
		}

		// TODO filter down to requested fields only (not important, just saves bandwidth)
		responseArgs := map[string]interface{}{
			"torrents": allTorrents,
		}

		if selection.Mode == DataSource.TSMODE_RECENTLY_ACTIVE {
			// also need to include a 'removed' key for recently-removed torrents
			responseArgs["removed"] = make([]int64, 0)
		}

		return trpcResponse{
			Arguments: responseArgs,
			Result:    "success",
			Tag:       req.Tag,
		}

	default:
		return ErrorReponse(req.Tag, "Unknown method")

	}
}

func (this *Server) ServeMux() *http.ServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc(this.baseDir+"rpc", this.HandleRPCRequest)
	mux.Handle(this.baseDir, http.FileServer(assetFS()))
	return mux
}
