package services

import (
	"context"
	"crypto/tls"
	"io"
	"log"
	"os"
	"strings"
	"sync"

	"golang.org/x/text/encoding"

	"github.com/docker/docker/api/types"
	"github.com/docker/docker/client"
)

var rw sync.Mutex

type UInt16Slice []uint16

func (p UInt16Slice) Len() int           { return len(p) }
func (p UInt16Slice) Less(i, j int) bool { return p[i] < p[j] }
func (p UInt16Slice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

type Instance struct {
	session      *Session                `json:"-"`
	Name         string                  `json:"name"`
	Hostname     string                  `json:"hostname"`
	IP           string                  `json:"ip"`
	conn         *types.HijackedResponse `json:"-"`
	ctx          context.Context         `json:"-"`
	dockerClient *client.Client          `json:"-"`
	IsManager    *bool                   `json:"is_manager"`
	Mem          string                  `json:"mem"`
	Cpu          string                  `json:"cpu"`
	Ports        UInt16Slice
	tempPorts    []uint16         `json:"-"`
	ServerCert   []byte           `json:"server_cert"`
	ServerKey    []byte           `json:"server_key"`
	cert         *tls.Certificate `json:"-"`
}

func (i *Instance) setUsedPort(port uint16) {
	rw.Lock()
	defer rw.Unlock()

	for _, p := range i.tempPorts {
		if p == port {
			return
		}
	}
	i.tempPorts = append(i.tempPorts, port)
}

func (i *Instance) SetCertificate(cert, key []byte) (*tls.Certificate, error) {
	i.ServerCert = cert
	i.ServerKey = key
	c, e := tls.X509KeyPair(i.ServerCert, i.ServerKey)
	if e != nil {
		return nil, e
	}
	i.cert = &c

	// We store sessions as soon as we set instance keys
	if err := saveSessionsToDisk(); err != nil {
		return nil, err
	}
	return i.cert, nil
}
func (i *Instance) GetCertificate() *tls.Certificate {
	return i.cert
}

func (i *Instance) IsConnected() bool {
	return i.conn != nil

}

func (i *Instance) SetSession(s *Session) {
	i.session = s
}

var dindImage string

func init() {
	dindImage = getDindImageName()
}

func getDindImageName() string {
	dindImage := os.Getenv("DIND_IMAGE")
	defaultDindImageName := "franela/dind"
	if len(dindImage) == 0 {
		dindImage = defaultDindImageName
	}
	return dindImage
}

func NewInstance(session *Session, imageName string) (*Instance, error) {
	if imageName == "" {
		imageName = dindImage
	}
	log.Printf("NewInstance - using image: [%s]\n", imageName)
	instance, err := CreateInstance(session, imageName)
	if err != nil {
		return nil, err
	}
	instance.session = session

	if session.Instances == nil {
		session.Instances = make(map[string]*Instance)
	}
	session.Instances[instance.Name] = instance

	go instance.Attach()

	err = saveSessionsToDisk()
	if err != nil {
		return nil, err
	}

	wsServer.BroadcastTo(session.Id, "new instance", instance.Name, instance.IP, instance.Hostname)

	setGauges()

	return instance, nil
}

type sessionWriter struct {
	instance *Instance
}

func (s *sessionWriter) Write(p []byte) (n int, err error) {
	wsServer.BroadcastTo(s.instance.session.Id, "terminal out", s.instance.Name, string(p))
	return len(p), nil
}

func (i *Instance) ResizeTerminal(cols, rows uint) error {
	return ResizeConnection(i.Name, cols, rows)
}

func (i *Instance) Attach() {
	i.ctx = context.Background()
	conn, err := CreateAttachConnection(i.Name, i.ctx)

	if err != nil {
		return
	}

	i.conn = conn

	go func() {
		encoder := encoding.Replacement.NewEncoder()
		sw := &sessionWriter{instance: i}
		io.Copy(encoder.Writer(sw), conn.Reader)
	}()

	select {
	case <-i.ctx.Done():
	}
}
func GetInstance(session *Session, name string) *Instance {
	return session.Instances[name]
}

func FindInstanceByIP(ip string) *Instance {
	for _, s := range sessions {
		for _, i := range s.Instances {
			if i.IP == ip {
				return i
			}
		}
	}
	return nil
}

func DeleteInstance(session *Session, instance *Instance) error {
	if instance.conn != nil {
		instance.conn.Close()
	}
	err := DeleteContainer(instance.Name)
	if err != nil && !strings.Contains(err.Error(), "No such container") {
		log.Println(err)
		return err
	}

	wsServer.BroadcastTo(session.Id, "delete instance", instance.Name)

	delete(session.Instances, instance.Name)
	if err := saveSessionsToDisk(); err != nil {
		return err
	}
	setGauges()

	return nil
}
