package model

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/cuigh/auxo/byte/size"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/mount"
	"github.com/docker/docker/api/types/swarm"
)

type Registry struct {
	ID        string    `bson:"_id" json:"id,omitempty"`
	Name      string    `bson:"name" json:"name,omitempty" valid:"required"`
	URL       string    `bson:"url" json:"url,omitempty" valid:"required,url"`
	Username  string    `bson:"username" json:"username,omitempty" valid:"required"`
	Password  string    `bson:"password" json:"password,omitempty" valid:"required"`
	CreatedAt time.Time `bson:"created_at" json:"created_at,omitempty"`
	UpdatedAt time.Time `bson:"updated_at" json:"updated_at,omitempty"`
}

func (r *Registry) Match(image string) bool {
	return strings.HasPrefix(image, r.URL)
}

func (r *Registry) GetEncodedAuth() string {
	cfg := &types.AuthConfig{
		ServerAddress: r.URL,
		Username:      r.Username,
		Password:      r.Password,
	}
	if buf, e := json.Marshal(cfg); e == nil {
		return base64.URLEncoding.EncodeToString(buf)
	}
	return ""
}

type Option struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

type Options []*Option

func NewOptions(m map[string]string) Options {
	if len(m) == 0 {
		return nil
	}

	opts := Options{}
	for k, v := range m {
		opts = append(opts, &Option{Name: k, Value: v})
	}
	sort.Slice(opts, func(i, j int) bool {
		return opts[i].Name < opts[j].Name
	})
	return opts
}

func (opts Options) ToMap() map[string]string {
	if len(opts) == 0 {
		return nil
	}

	m := make(map[string]string)
	for _, opt := range opts {
		if opt != nil && opt.Name != "" && opt.Value != "" {
			m[opt.Name] = opt.Value
		}
	}
	return m
}

func (opts Options) Compress() Options {
	if len(opts) > 0 {
		var tmp Options
		for _, opt := range opts {
			if opt != nil {
				tmp = append(tmp, opt)
			}
		}
		return tmp
	}
	return opts
}

type ServiceListInfo struct {
	Name         string
	Image        string
	Mode         string
	Actives      uint64
	Replicas     uint64
	Rollback     bool
	UpdatedAt    time.Time
	UpdateStatus string
}

func NewServiceListInfo(service swarm.Service, actives uint64) *ServiceListInfo {
	info := &ServiceListInfo{
		Name:      service.Spec.Name,
		Image:     normalizeImage(service.Spec.TaskTemplate.ContainerSpec.Image),
		Actives:   actives,
		UpdatedAt: service.UpdatedAt.Local(),
		Rollback:  service.PreviousSpec != nil,
	}
	if service.UpdateStatus != nil {
		info.UpdateStatus = string(service.UpdateStatus.State)
	}
	if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
		info.Mode = "replicated"
		info.Replicas = *service.Spec.Mode.Replicated.Replicas
	} else if service.Spec.Mode.Global != nil {
		info.Mode = "global"
		// info.Replicas = tasksNoShutdown[service.ID]
	}
	return info
}

type ServiceDetailInfo struct {
	swarm.Service
	// Name      string
	// Image     string
	// Mode      string
	// Actives   uint64
	Replicas uint64
	// UpdatedAt time.Time
	Env      map[string]string
	Networks []Network
	Command  string
	Args     string
}

type Network struct {
	ID      string
	Name    string
	Address string
}

func NewServiceDetailInfo(service swarm.Service) *ServiceDetailInfo {
	info := &ServiceDetailInfo{
		Service: service,
		Command: strings.Join(service.Spec.TaskTemplate.ContainerSpec.Command, " "),
		Args:    strings.Join(service.Spec.TaskTemplate.ContainerSpec.Args, " "),
	}
	if service.Spec.Mode.Replicated != nil {
		info.Replicas = *service.Spec.Mode.Replicated.Replicas
	}
	if len(service.Spec.TaskTemplate.ContainerSpec.Env) > 0 {
		info.Env = make(map[string]string)
		for _, env := range service.Spec.TaskTemplate.ContainerSpec.Env {
			pair := strings.SplitN(env, "=", 2)
			if len(pair) == 2 {
				info.Env[pair[0]] = pair[1]
			}
		}
	}
	return info
}

type ServiceInfo struct {
	Name            string       `json:"name"`
	Version         uint64       `json:"version"`
	Registry        string       `json:"registry"`
	RegistryURL     string       `json:"-"`
	RegistryAuth    string       `json:"-"`
	Image           string       `json:"image"`
	Mode            string       `json:"mode"`
	Replicas        uint64       `json:"replicas"`
	Command         string       `json:"command"`
	Args            string       `json:"args"`
	Dir             string       `json:"dir"`
	User            string       `json:"user"`
	Networks        []string     `json:"networks"`
	Secrets         []ConfigInfo `json:"secrets"`
	Configs         []ConfigInfo `json:"configs"`
	Environments    Options      `json:"envs"`
	ServiceLabels   Options      `json:"slabels"`
	ContainerLabels Options      `json:"clabels"`
	Endpoint        struct {
		Mode  swarm.ResolutionMode `json:"mode"`
		Ports []EndpointPort       `json:"ports"`
	} `json:"endpoint"`
	Mounts    []Mount `json:"mounts"`
	LogDriver struct {
		Name    string  `json:"name"`
		Options Options `json:"options"`
	} `json:"log_driver"`
	Placement struct {
		Constraints []PlacementConstraint `json:"constraints"`
		Preferences []PlacementPreference `json:"preferences"`
	} `json:"placement"`
	UpdatePolicy struct {
		// Maximum number of tasks updated simultaneously (0 to update all at once), default 1.
		Parallelism uint64 `json:"parallelism"`
		// Amount of time between updates.
		Delay string `json:"delay,omitempty"`
		// FailureAction is the action to take when an update failures. (“pause”|“continue”|“rollback”) (default “pause”)
		FailureAction string `json:"failure_action"`
		// Update order (“start-first”|“stop-first”) (default “stop-first”)
		Order string `json:"order"`
	} `json:"update_policy"`
	RollbackPolicy struct {
		// Maximum number of tasks updated simultaneously (0 to update all at once), default 1.
		Parallelism uint64 `json:"parallelism"`
		// Amount of time between updates.
		Delay string `json:"delay,omitempty"`
		// FailureAction is the action to take when an update failures. (“pause”|“continue”) (default “pause”)
		FailureAction string `json:"failure_action"`
		// Update order (“start-first”|“stop-first”) (default “stop-first”)
		Order string `json:"order"`
	} `json:"rollback_policy"`
	RestartPolicy struct {
		// Restart when condition is met (“none”|“on-failure”|“any”) (default “any”)
		Condition swarm.RestartPolicyCondition `json:"condition"`
		// Maximum number of restarts before giving up
		MaxAttempts uint64 `json:"max_attempts"`
		// Delay between restart attempts
		Delay string `json:"delay,omitempty"`
		// Window used to evaluate the restart policy.
		Window string `json:"window,omitempty"`
	} `json:"restart_policy"`
	Resource struct {
		Limit   ResourceInfo `json:"limit"`
		Reserve ResourceInfo `json:"reserve"`
	} `json:"resource"`
	DNS struct {
		Nameservers string `json:"nameservers,omitempty"`
		Search      string `json:"search,omitempty"`
		Options     string `json:"options,omitempty"`
	} `json:"dns"`
	Hostname string `json:"hostname"`
	Hosts    string `json:"hosts"`
}

func NewServiceInfo(service swarm.Service) *ServiceInfo {
	spec := service.Spec
	si := &ServiceInfo{
		Name:    spec.Name,
		Version: service.Version.Index,
		//Hostname:    spec.TaskTemplate.ContainerSpec.Hostname,
		Image:           spec.TaskTemplate.ContainerSpec.Image,
		Command:         strings.Join(spec.TaskTemplate.ContainerSpec.Command, " "),
		Args:            strings.Join(spec.TaskTemplate.ContainerSpec.Args, " "),
		Dir:             spec.TaskTemplate.ContainerSpec.Dir,
		User:            spec.TaskTemplate.ContainerSpec.User,
		ServiceLabels:   NewOptions(spec.Labels),
		ContainerLabels: NewOptions(spec.TaskTemplate.ContainerSpec.Labels),
	}
	for _, n := range service.Spec.TaskTemplate.Networks {
		si.Networks = append(si.Networks, n.Target)
	}
	if spec.EndpointSpec != nil {
		si.Endpoint.Mode = spec.EndpointSpec.Mode
		for _, p := range spec.EndpointSpec.Ports {
			si.Endpoint.Ports = append(si.Endpoint.Ports, EndpointPort{
				Protocol:      p.Protocol,
				TargetPort:    p.TargetPort,
				PublishedPort: p.PublishedPort,
				PublishMode:   p.PublishMode,
			})
		}
	}
	if spec.Mode.Global != nil {
		si.Mode = "global"
	} else if spec.Mode.Replicated != nil {
		si.Mode, si.Replicas = "replicated", *spec.Mode.Replicated.Replicas
	}
	if len(spec.TaskTemplate.ContainerSpec.Env) > 0 {
		si.Environments = Options{}
		for _, env := range spec.TaskTemplate.ContainerSpec.Env {
			pair := strings.SplitN(env, "=", 2)
			si.Environments = append(si.Environments, &Option{Name: pair[0], Value: pair[1]})
		}
	}
	for _, m := range spec.TaskTemplate.ContainerSpec.Mounts {
		mnt := Mount{
			Type:     m.Type,
			Source:   m.Source,
			Target:   m.Target,
			ReadOnly: m.ReadOnly,
		}
		if m.BindOptions != nil {
			mnt.Propagation = m.BindOptions.Propagation
		}
		si.Mounts = append(si.Mounts, mnt)
	}
	if spec.TaskTemplate.Resources != nil {
		si.Resource.Limit = NewResourceInfo(spec.TaskTemplate.Resources.Limits)
		si.Resource.Reserve = NewResourceInfo(spec.TaskTemplate.Resources.Reservations)
	}
	if spec.TaskTemplate.LogDriver != nil {
		si.LogDriver.Name = spec.TaskTemplate.LogDriver.Name
		si.LogDriver.Options = NewOptions(spec.TaskTemplate.LogDriver.Options)
	}
	if spec.TaskTemplate.Placement != nil {
		for _, c := range spec.TaskTemplate.Placement.Constraints {
			si.Placement.Constraints = append(si.Placement.Constraints, NewPlacementConstraint(c))
		}
		for _, p := range spec.TaskTemplate.Placement.Preferences {
			si.Placement.Preferences = append(si.Placement.Preferences, PlacementPreference{Spread: p.Spread.SpreadDescriptor})
		}
	}
	if spec.UpdateConfig != nil {
		si.UpdatePolicy.Parallelism = spec.UpdateConfig.Parallelism
		if spec.UpdateConfig.Delay > 0 {
			si.UpdatePolicy.Delay = spec.UpdateConfig.Delay.String()
		}
		si.UpdatePolicy.FailureAction = spec.UpdateConfig.FailureAction
		si.UpdatePolicy.Order = spec.UpdateConfig.Order
	}
	if spec.RollbackConfig != nil {
		si.RollbackPolicy.Parallelism = spec.RollbackConfig.Parallelism
		if spec.RollbackConfig.Delay > 0 {
			si.RollbackPolicy.Delay = spec.RollbackConfig.Delay.String()
		}
		si.RollbackPolicy.FailureAction = spec.RollbackConfig.FailureAction
		si.RollbackPolicy.Order = spec.RollbackConfig.Order
	}
	if spec.TaskTemplate.RestartPolicy != nil {
		si.RestartPolicy.Condition = spec.TaskTemplate.RestartPolicy.Condition
		if spec.TaskTemplate.RestartPolicy.MaxAttempts != nil {
			si.RestartPolicy.MaxAttempts = *spec.TaskTemplate.RestartPolicy.MaxAttempts
		}
		if spec.TaskTemplate.RestartPolicy.Delay != nil && *spec.TaskTemplate.RestartPolicy.Delay > 0 {
			si.RestartPolicy.Delay = spec.TaskTemplate.RestartPolicy.Delay.String()
		}
		if spec.TaskTemplate.RestartPolicy.Window != nil && *spec.TaskTemplate.RestartPolicy.Window > 0 {
			si.RestartPolicy.Window = spec.TaskTemplate.RestartPolicy.Window.String()
		}
	}
	for _, s := range spec.TaskTemplate.ContainerSpec.Secrets {
		secret := NewSecretInfo(s)
		si.Secrets = append(si.Secrets, secret)
	}
	for _, c := range spec.TaskTemplate.ContainerSpec.Configs {
		config := NewConfigInfo(c)
		si.Configs = append(si.Configs, config)
	}
	if dns := spec.TaskTemplate.ContainerSpec.DNSConfig; dns != nil {
		si.DNS.Nameservers = strings.Join(dns.Nameservers, ",")
		si.DNS.Search = strings.Join(dns.Search, ",")
		si.DNS.Options = strings.Join(dns.Options, ",")
	}
	si.Hostname = spec.TaskTemplate.ContainerSpec.Hostname
	if len(spec.TaskTemplate.ContainerSpec.Hosts) > 0 {
		si.Hosts = strings.Join(spec.TaskTemplate.ContainerSpec.Hosts, "\n")
	}
	return si
}

// TODO: finish this method
func (si *ServiceInfo) ToServiceSpec() swarm.ServiceSpec {
	return swarm.ServiceSpec{}
}

func (si *ServiceInfo) Normalize() {
	si.Environments = si.Environments.Compress()
	si.ServiceLabels = si.ServiceLabels.Compress()
	si.ContainerLabels = si.ContainerLabels.Compress()
	si.LogDriver.Options = si.LogDriver.Options.Compress()
	if len(si.Configs) > 0 {
		var tmp []ConfigInfo
		for _, c := range si.Configs {
			if c.ID != "" {
				tmp = append(tmp, c)
			}
		}
		si.Configs = tmp
	}
	if len(si.Secrets) > 0 {
		var tmp []ConfigInfo
		for _, c := range si.Secrets {
			if c.ID != "" {
				tmp = append(tmp, c)
			}
		}
		si.Secrets = tmp
	}
}

func (si *ServiceInfo) GetDNSConfig() *swarm.DNSConfig {
	if si.DNS.Nameservers == "" && si.DNS.Search == "" && si.DNS.Options == "" {
		return nil
	}

	c := &swarm.DNSConfig{}
	if si.DNS.Nameservers != "" {
		c.Nameservers = strings.Split(si.DNS.Nameservers, ",")
	}
	if si.DNS.Search != "" {
		c.Search = strings.Split(si.DNS.Search, ",")
	}
	if si.DNS.Options != "" {
		c.Options = strings.Split(si.DNS.Options, ",")
	}
	return c
}

type EndpointPort struct {
	Name     string                   `json:"name"`
	Protocol swarm.PortConfigProtocol `json:"protocol"`
	// TargetPort is the port inside the container
	TargetPort uint32 `json:"target_port"`
	// PublishedPort is the port on the swarm hosts
	PublishedPort uint32 `json:"published_port"`
	// PublishMode is the mode in which port is published
	PublishMode swarm.PortConfigPublishMode `json:"publish_mode"`
}

type Mount struct {
	Type        mount.Type        `json:"type"`
	Source      string            `json:"src"`
	Target      string            `json:"dst"`
	ReadOnly    bool              `json:"read_only"`
	Propagation mount.Propagation `json:"propagation"`
}

type ResourceInfo struct {
	CPU    float64 `json:"cpu"`
	Memory string  `json:"memory"`
}

func NewResourceInfo(res *swarm.Resources) ResourceInfo {
	ri := ResourceInfo{}
	if res != nil {
		ri.CPU = float64(res.NanoCPUs) / 1e9
		if res.MemoryBytes > 0 {
			ri.Memory = size.Size(res.MemoryBytes).String()
		}
	}
	return ri
}

func (r ResourceInfo) IsSet() bool {
	return r.CPU > 0 || r.Memory != ""
}

func (r ResourceInfo) ToResources() (res *swarm.Resources, err error) {
	res = &swarm.Resources{
		NanoCPUs: int64(r.CPU * 1e9),
	}
	if r.Memory != "" {
		var s size.Size
		if s, err = size.Parse(r.Memory); err != nil {
			return nil, err
		}
		res.MemoryBytes = int64(s)
	}
	return
}

type ConfigInfo struct {
	ID       string `json:"id"`
	Name     string `json:"name"`
	FileName string `json:"file_name"`
	UID      string `json:"uid"`
	GID      string `json:"gid"`
	Mode     uint32 `json:"mode"`
}

func NewSecretInfo(ref *swarm.SecretReference) ConfigInfo {
	mode, _ := strconv.ParseUint(strconv.FormatUint(uint64(ref.File.Mode), 8), 10, 32)
	return ConfigInfo{
		ID:       ref.SecretID,
		Name:     ref.SecretName,
		FileName: ref.File.Name,
		UID:      ref.File.UID,
		GID:      ref.File.GID,
		Mode:     uint32(mode),
	}
}

func NewConfigInfo(ref *swarm.ConfigReference) ConfigInfo {
	mode, _ := strconv.ParseUint(strconv.FormatUint(uint64(ref.File.Mode), 8), 10, 32)
	return ConfigInfo{
		ID:       ref.ConfigID,
		Name:     ref.ConfigName,
		FileName: ref.File.Name,
		UID:      ref.File.UID,
		GID:      ref.File.GID,
		Mode:     uint32(mode),
	}
}

func (ci ConfigInfo) ToSecret() *swarm.SecretReference {
	mode, _ := strconv.ParseUint(strconv.FormatUint(uint64(ci.Mode), 10), 8, 32)
	return &swarm.SecretReference{
		SecretID:   ci.ID,
		SecretName: ci.Name,
		File: &swarm.SecretReferenceFileTarget{
			Name: ci.FileName,
			UID:  ci.UID,
			GID:  ci.GID,
			Mode: os.FileMode(mode),
		},
	}
}

func (ci ConfigInfo) ToConfig() *swarm.ConfigReference {
	mode, _ := strconv.ParseUint(strconv.FormatUint(uint64(ci.Mode), 10), 8, 32)
	return &swarm.ConfigReference{
		ConfigID:   ci.ID,
		ConfigName: ci.Name,
		File: &swarm.ConfigReferenceFileTarget{
			Name: ci.FileName,
			UID:  ci.UID,
			GID:  ci.GID,
			Mode: os.FileMode(mode),
		},
	}
}

type PlacementPreference struct {
	Spread string `json:"spread"`
}

type PlacementConstraint struct {
	Name     string `json:"name"`
	Value    string `json:"value"`
	Operator string `json:"op"`
}

func NewPlacementConstraint(c string) PlacementConstraint {
	var (
		pc    = PlacementConstraint{}
		items []string
	)

	if items = strings.SplitN(c, "==", 2); len(items) == 2 {
		pc.Operator = "=="
	} else if items = strings.SplitN(c, "!=", 2); len(items) == 2 {
		pc.Operator = "!="
	}
	if pc.Operator != "" {
		pc.Name = strings.TrimSpace(items[0])
		pc.Value = strings.TrimSpace(items[1])
	}

	return pc
}

func (pc *PlacementConstraint) ToConstraint() string {
	if pc.Name != "" && pc.Value != "" && pc.Operator != "" {
		return fmt.Sprintf("%s %s %s", pc.Name, pc.Operator, pc.Value)
	}
	return ""
}

type ConfigCreateInfo struct {
	Name   string  `json:"name"`
	Data   string  `json:"data"`
	Labels Options `json:"labels"`
}

type ConfigUpdateInfo struct {
	ID      string `json:"id"`
	Version uint64 `json:"version"`
	ConfigCreateInfo
}

type TaskListArgs struct {
	Service   string `bind:"service"`
	Node      string `bind:"node"`
	Name      string `bind:"name"`
	State     string `bind:"state"`
	PageIndex int    `bind:"page"`
	PageSize  int    `bind:"size"`
}

type TaskInfo struct {
	swarm.Task
	NodeName string
	Image    string
}

func NewTaskInfo(task swarm.Task, nodeName string) *TaskInfo {
	return &TaskInfo{
		Task:     task,
		NodeName: nodeName,
		Image:    normalizeImage(task.Spec.ContainerSpec.Image),
	}
}

type NodeListInfo struct {
	ID      string
	Name    string
	Role    swarm.NodeRole
	Version string
	CPU     int64
	Memory  float32
	Address string
	Status  swarm.NodeState
	Leader  bool
}

func NewNodeListInfo(node swarm.Node) *NodeListInfo {
	info := &NodeListInfo{
		ID:      node.ID,
		Name:    node.Spec.Name,
		Role:    node.Spec.Role,
		Version: node.Description.Engine.EngineVersion,
		CPU:     node.Description.Resources.NanoCPUs / 1e9,
		Memory:  float32(node.Description.Resources.MemoryBytes>>20) / 1024,
		Address: node.Status.Addr,
		Status:  node.Status.State,
		Leader:  node.ManagerStatus != nil && node.ManagerStatus.Leader,
	}
	if info.Name == "" {
		info.Name = node.Description.Hostname
	}
	return info
}

type NodeUpdateInfo struct {
	Version      uint64                 `json:"version"`
	Name         string                 `json:"name"`
	Role         swarm.NodeRole         `json:"role"`
	Availability swarm.NodeAvailability `json:"availability"`
	Labels       Options                `json:"labels"`
}

type NetworkCreateInfo struct {
	Name         string `json:"name"`
	Driver       string `json:"driver"`
	CustomDriver string `json:"custom_driver"`
	Internal     bool   `json:"internal"`
	Attachable   bool
	IPV4         struct {
		Subnet  string `json:"subnet"`
		Gateway string `json:"gateway"`
		IPRange string `json:"ip_range"`
	} `json:"ipv4"`
	IPV6 struct {
		Enabled bool   `json:"enabled"`
		Subnet  string `json:"subnet"`
		Gateway string `json:"gateway"`
	} `json:"ipv6"`
	Options   Options `json:"options"`
	Labels    Options `json:"labels"`
	LogDriver struct {
		Name    string  `json:"name"`
		Options Options `json:"options"`
	} `json:"log_driver"`
}

type VolumeCreateInfo struct {
	Name         string  `json:"name" valid:"required"`
	Driver       string  `json:"driver"`
	CustomDriver string  `json:"custom_driver"`
	Options      Options `json:"options"`
	Labels       Options `json:"labels"`
}

type ImageListInfo struct {
	types.ImageSummary
	CreatedAt time.Time
}

func NewImageListInfo(image types.ImageSummary) *ImageListInfo {
	info := &ImageListInfo{
		ImageSummary: image,
		CreatedAt:    time.Unix(image.Created, 0),
	}
	return info
}

type ContainerListArgs struct {
	// created|restarting|running|removing|paused|exited|dead
	Filter    string `bind:"filter"`
	Name      string `bind:"name"`
	PageIndex int    `bind:"page"`
	PageSize  int    `bind:"size"`
}

type ContainerListInfo struct {
	types.Container
	CreatedAt time.Time
}

func NewContainerListInfo(container types.Container) *ContainerListInfo {
	info := &ContainerListInfo{
		Container: container,
		CreatedAt: time.Unix(container.Created, 0),
	}
	return info
}

func normalizeImage(image string) string {
	// remove hash added by docker
	if i := strings.Index(image, "@sha256:"); i > 0 {
		image = image[:i]
	}
	return image
}
