package provisioner

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"path/filepath"
	"strings"

	lru "github.com/hashicorp/golang-lru"
	"github.com/play-with-docker/play-with-docker/config"
	"github.com/play-with-docker/play-with-docker/docker"
	"github.com/play-with-docker/play-with-docker/id"
	"github.com/play-with-docker/play-with-docker/pwd/types"
	"github.com/play-with-docker/play-with-docker/router"
	"github.com/play-with-docker/play-with-docker/storage"
)

type DinD struct {
	factory   docker.FactoryApi
	storage   storage.StorageApi
	generator id.Generator
	cache     *lru.Cache
}

func NewDinD(generator id.Generator, f docker.FactoryApi, s storage.StorageApi) *DinD {
	c, _ := lru.New(5000)
	return &DinD{generator: generator, factory: f, storage: s, cache: c}
}

func checkHostnameExists(sessionId, hostname string, instances []*types.Instance) bool {
	exists := false
	for _, instance := range instances {
		if instance.Hostname == hostname {
			exists = true
			break
		}
	}
	return exists
}

func (d *DinD) InstanceNew(session *types.Session, conf types.InstanceConfig) (*types.Instance, error) {
	if conf.ImageName == "" {
		playground, err := d.storage.PlaygroundGet(session.PlaygroundId)
		if err != nil {
			return nil, err
		}
		conf.ImageName = playground.DefaultDinDInstanceImage
	}
	log.Printf("NewInstance - using image: [%s]\n", conf.ImageName)
	if conf.Hostname == "" {
		instances, err := d.storage.InstanceFindBySessionId(session.Id)
		if err != nil {
			return nil, err
		}
		var nodeName string
		for i := 1; ; i++ {
			nodeName = fmt.Sprintf("node%d", i)
			exists := checkHostnameExists(session.Id, nodeName, instances)
			if !exists {
				break
			}
		}
		conf.Hostname = nodeName
	}

	networks := []string{session.Id}
	if config.Unsafe {
		networks = append(networks, conf.Networks...)
	}

	containerName := fmt.Sprintf("%s_%s", session.Id[:8], d.generator.NewId())
	opts := docker.CreateContainerOpts{
		Image:          conf.ImageName,
		SessionId:      session.Id,
		ContainerName:  containerName,
		Hostname:       conf.Hostname,
		ServerCert:     conf.ServerCert,
		ServerKey:      conf.ServerKey,
		CACert:         conf.CACert,
		HostFQDN:       conf.PlaygroundFQDN,
		Privileged:     true,
		Networks:       networks,
		DindVolumeSize: conf.DindVolumeSize,
		Envs:           conf.Envs,
	}

	dockerClient, err := d.factory.GetForSession(session)
	if err != nil {
		return nil, err
	}
	if err := dockerClient.ContainerCreate(opts); err != nil {
		return nil, err
	}

	ips, err := dockerClient.ContainerIPs(containerName)
	if err != nil {
		return nil, err
	}

	instance := &types.Instance{}
	instance.Image = opts.Image
	instance.IP = ips[session.Id]
	instance.RoutableIP = instance.IP
	instance.SessionId = session.Id
	instance.Name = containerName
	instance.Hostname = conf.Hostname
	instance.Cert = conf.Cert
	instance.Key = conf.Key
	instance.ServerCert = conf.ServerCert
	instance.ServerKey = conf.ServerKey
	instance.CACert = conf.CACert
	instance.Tls = conf.Tls
	instance.ProxyHost = router.EncodeHost(session.Id, instance.RoutableIP, router.HostOpts{})
	instance.SessionHost = session.Host

	return instance, nil
}

func (d *DinD) getSession(sessionId string) (*types.Session, error) {
	var session *types.Session
	if s, found := d.cache.Get(sessionId); !found {
		s, err := d.storage.SessionGet(sessionId)
		if err != nil {
			return nil, err
		}
		session = s
		d.cache.Add(sessionId, s)
	} else {
		session = s.(*types.Session)
	}
	return session, nil
}

func (d *DinD) InstanceDelete(session *types.Session, instance *types.Instance) error {
	dockerClient, err := d.factory.GetForSession(session)
	if err != nil {
		return err
	}
	err = dockerClient.ContainerDelete(instance.Name)
	if err != nil && !strings.Contains(err.Error(), "No such container") {
		return err
	}
	return nil
}

func (d *DinD) InstanceExec(instance *types.Instance, cmd []string) (int, error) {
	session, err := d.getSession(instance.SessionId)
	if err != nil {
		return -1, err
	}
	dockerClient, err := d.factory.GetForSession(session)
	if err != nil {
		return -1, err
	}
	return dockerClient.Exec(instance.Name, cmd)
}

func (d *DinD) InstanceFSTree(instance *types.Instance) (io.Reader, error) {
	session, err := d.getSession(instance.SessionId)
	if err != nil {
		return nil, err
	}
	dockerClient, err := d.factory.GetForSession(session)
	if err != nil {
		return nil, err
	}
	b := bytes.NewBuffer([]byte{})

	if c, err := dockerClient.ExecAttach(instance.Name, []string{"bash", "-c", `tree --noreport -J $HOME`}, b); c > 0 {
		log.Println(b.String())
		return nil, fmt.Errorf("Error %d trying list directories", c)
	} else if err != nil {
		return nil, err
	}

	return b, nil
}

func (d *DinD) InstanceFile(instance *types.Instance, filePath string) (io.Reader, error) {
	session, err := d.getSession(instance.SessionId)
	if err != nil {
		return nil, err
	}
	dockerClient, err := d.factory.GetForSession(session)
	if err != nil {
		return nil, err
	}

	return dockerClient.CopyFromContainer(instance.Name, filePath)
}

func (d *DinD) InstanceResizeTerminal(instance *types.Instance, rows, cols uint) error {
	session, err := d.getSession(instance.SessionId)
	if err != nil {
		return err
	}
	dockerClient, err := d.factory.GetForSession(session)
	if err != nil {
		return err
	}
	return dockerClient.ContainerResize(instance.Name, rows, cols)
}

func (d *DinD) InstanceGetTerminal(instance *types.Instance) (net.Conn, error) {
	session, err := d.getSession(instance.SessionId)
	if err != nil {
		return nil, err
	}
	dockerClient, err := d.factory.GetForSession(session)
	if err != nil {
		return nil, err
	}
	return dockerClient.CreateAttachConnection(instance.Name)
}

func (d *DinD) InstanceUploadFromUrl(instance *types.Instance, fileName, dest, url string) error {
	log.Printf("Downloading file [%s]\n", url)
	resp, err := http.Get(url)
	if err != nil {
		return fmt.Errorf("Could not download file [%s]. Error: %s\n", url, err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return fmt.Errorf("Could not download file [%s]. Status code: %d\n", url, resp.StatusCode)
	}
	session, err := d.getSession(instance.SessionId)
	if err != nil {
		return err
	}
	dockerClient, err := d.factory.GetForSession(session)
	if err != nil {
		return err
	}

	copyErr := dockerClient.CopyToContainer(instance.Name, dest, fileName, resp.Body)

	if copyErr != nil {
		return fmt.Errorf("Error while downloading file [%s]. Error: %s\n", url, copyErr)
	}

	return nil
}

func (d *DinD) getInstanceCWD(instance *types.Instance) (string, error) {
	session, err := d.getSession(instance.SessionId)
	if err != nil {
		return "", err
	}
	dockerClient, err := d.factory.GetForSession(session)
	if err != nil {
		return "", err
	}
	b := bytes.NewBufferString("")

	if c, err := dockerClient.ExecAttach(instance.Name, []string{"bash", "-c", `pwdx $(</var/run/cwd)`}, b); c > 0 {
		return "", fmt.Errorf("Error %d trying to get CWD", c)
	} else if err != nil {
		return "", err
	}

	cwd := strings.TrimSpace(strings.Split(b.String(), ":")[1])

	return cwd, nil
}

func (d *DinD) InstanceUploadFromReader(instance *types.Instance, fileName, dest string, reader io.Reader) error {
	session, err := d.getSession(instance.SessionId)
	if err != nil {
		return err
	}
	dockerClient, err := d.factory.GetForSession(session)
	if err != nil {
		return err
	}
	var finalDest string
	if filepath.IsAbs(dest) {
		finalDest = dest
	} else {
		if cwd, err := d.getInstanceCWD(instance); err != nil {
			return err
		} else {
			finalDest = fmt.Sprintf("%s/%s", cwd, dest)
		}
	}

	copyErr := dockerClient.CopyToContainer(instance.Name, finalDest, fileName, reader)

	if copyErr != nil {
		return fmt.Errorf("Error while uploading file [%s]. Error: %s\n", fileName, copyErr)
	}

	return nil
}
