package metadata

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"time"
)

type Client interface {
	OnChangeWithError(int, func(string)) error
	OnChange(int, func(string))
	OnChangeCtx(context.Context, int, func(string))
	SendRequest(string) ([]byte, error)
	GetVersion() (string, error)
	GetSelfHost() (Host, error)
	GetSelfContainer() (Container, error)
	GetSelfServiceByName(string) (Service, error)
	GetSelfService() (Service, error)
	GetSelfStack() (Stack, error)
	GetServices() ([]Service, error)
	GetStacks() ([]Stack, error)
	GetStackByName(string) (Stack, error)
	GetContainers() ([]Container, error)
	GetServiceContainers(string, string) ([]Container, error)
	GetHosts() ([]Host, error)
	GetHost(string) (Host, error)
	GetNetworks() ([]Network, error)
}

type client struct {
	url    string
	ip     string
	client *http.Client
}

func newClient(url, ip string) *client {
	return &client{url, ip, &http.Client{Timeout: 10 * time.Second}}
}

func NewClient(url string) Client {
	ip := ""
	return newClient(url, ip)
}

func NewClientWithIPAndWait(url, ip string) (Client, error) {
	client := newClient(url, ip)

	if err := testConnection(client); err != nil {
		return nil, err
	}

	return client, nil
}

func NewClientAndWait(url string) (Client, error) {
	ip := ""
	client := newClient(url, ip)

	if err := testConnection(client); err != nil {
		return nil, err
	}

	return client, nil
}

func (m *client) SendRequest(path string) ([]byte, error) {
	req, err := http.NewRequest("GET", m.url+path, nil)
	if err != nil {
		return nil, err
	}

	req.Header.Add("Accept", "application/json")
	if m.ip != "" {
		req.Header.Add("X-Forwarded-For", m.ip)
	}

	resp, err := m.client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("Error %v accessing %v path", resp.StatusCode, path)
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	return body, nil
}

func (m *client) SendRequestCtx(ctx context.Context, path string) ([]byte, error) {
	req, err := http.NewRequest("GET", m.url+path, nil)
	if err != nil {
		return nil, err
	}

	req.Header.Add("Accept", "application/json")
	if m.ip != "" {
		req.Header.Add("X-Forwarded-For", m.ip)
	}

	resp, err := m.client.Do(req.WithContext(ctx))
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("error %v accessing %v path", resp.StatusCode, path)
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	return body, nil
}

func (m *client) GetVersion() (string, error) {
	resp, err := m.SendRequest("/version")
	if err != nil {
		return "", err
	}
	return string(resp[:]), nil
}

func (m *client) GetSelfHost() (Host, error) {
	resp, err := m.SendRequest("/self/host")
	var host Host
	if err != nil {
		return host, err
	}

	if err = json.Unmarshal(resp, &host); err != nil {
		return host, err
	}

	return host, nil
}

func (m *client) GetSelfContainer() (Container, error) {
	resp, err := m.SendRequest("/self/container")
	var container Container
	if err != nil {
		return container, err
	}

	if err = json.Unmarshal(resp, &container); err != nil {
		return container, err
	}

	return container, nil
}

func (m *client) GetSelfServiceByName(name string) (Service, error) {
	resp, err := m.SendRequest("/self/stack/services/" + name)
	var service Service
	if err != nil {
		return service, err
	}

	if err = json.Unmarshal(resp, &service); err != nil {
		return service, err
	}

	return service, nil
}

func (m *client) GetSelfService() (Service, error) {
	resp, err := m.SendRequest("/self/service")
	var service Service
	if err != nil {
		return service, err
	}

	if err = json.Unmarshal(resp, &service); err != nil {
		return service, err
	}

	return service, nil
}

func (m *client) GetSelfStack() (Stack, error) {
	resp, err := m.SendRequest("/self/stack")
	var stack Stack
	if err != nil {
		return stack, err
	}

	if err = json.Unmarshal(resp, &stack); err != nil {
		return stack, err
	}

	return stack, nil
}

func (m *client) GetServices() ([]Service, error) {
	resp, err := m.SendRequest("/services")
	var services []Service
	if err != nil {
		return services, err
	}

	if err = json.Unmarshal(resp, &services); err != nil {
		return services, err
	}
	return services, nil
}

func (m *client) GetStacks() ([]Stack, error) {
	resp, err := m.SendRequest("/stacks")
	var stacks []Stack
	if err != nil {
		return stacks, err
	}

	if err = json.Unmarshal(resp, &stacks); err != nil {
		return stacks, err
	}
	return stacks, nil
}

func (m *client) GetStackByName(name string) (Stack, error) {
	resp, err := m.SendRequest("/stacks/" + name)
	var stack Stack
	if err != nil {
		return stack, err
	}

	if err = json.Unmarshal(resp, &stack); err != nil {
		return stack, err
	}

	return stack, nil
}

func (m *client) GetContainers() ([]Container, error) {
	resp, err := m.SendRequest("/containers")
	var containers []Container
	if err != nil {
		return containers, err
	}

	if err = json.Unmarshal(resp, &containers); err != nil {
		return containers, err
	}
	return containers, nil
}

func (m *client) GetServiceContainers(serviceName string, stackName string) ([]Container, error) {
	var serviceContainers = []Container{}
	containers, err := m.GetContainers()
	if err != nil {
		return serviceContainers, err
	}

	for _, container := range containers {
		if container.StackName == stackName && container.ServiceName == serviceName {
			serviceContainers = append(serviceContainers, container)
		}
	}

	return serviceContainers, nil
}

func (m *client) GetHosts() ([]Host, error) {
	resp, err := m.SendRequest("/hosts")
	var hosts []Host
	if err != nil {
		return hosts, err
	}

	if err = json.Unmarshal(resp, &hosts); err != nil {
		return hosts, err
	}
	return hosts, nil
}

func (m *client) GetHost(UUID string) (Host, error) {
	var host Host
	hosts, err := m.GetHosts()
	if err != nil {
		return host, err
	}
	for _, host := range hosts {
		if host.UUID == UUID {
			return host, nil
		}
	}

	return host, fmt.Errorf("could not find host by UUID %v", UUID)
}

func (m *client) GetNetworks() ([]Network, error) {
	resp, err := m.SendRequest("/networks")
	var networks []Network
	if err != nil {
		return networks, err
	}

	if err = json.Unmarshal(resp, &networks); err != nil {
		return networks, err
	}

	return networks, nil
}
