package hub

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"net"
	"net/http"

	"github.com/traefik/traefik/v2/pkg/config/dynamic"
	"github.com/traefik/traefik/v2/pkg/log"
	"github.com/traefik/traefik/v2/pkg/provider"
	"github.com/traefik/traefik/v2/pkg/safe"
	ttls "github.com/traefik/traefik/v2/pkg/tls"
)

var _ provider.Provider = (*Provider)(nil)

// Entrypoints created for Hub.
const (
	APIEntrypoint    = "traefikhub-api"
	TunnelEntrypoint = "traefikhub-tunl"
)

// Provider holds configurations of the provider.
type Provider struct {
	TLS *TLS `description:"TLS configuration for mTLS communication between Traefik and Hub Agent." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`

	server *http.Server
}

// TLS configures the mTLS connection between Traefik Proxy and the Traefik Hub Agent.
type TLS struct {
	Insecure bool               `description:"Enables an insecure TLS connection that uses default credentials, and which has no peer authentication between Traefik Proxy and the Traefik Hub Agent." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
	CA       ttls.FileOrContent `description:"The certificate authority authenticates the Traefik Hub Agent certificate." json:"ca,omitempty" toml:"ca,omitempty" yaml:"ca,omitempty" loggable:"false"`
	Cert     ttls.FileOrContent `description:"The TLS certificate for Traefik Proxy as a TLS client." json:"cert,omitempty" toml:"cert,omitempty" yaml:"cert,omitempty" loggable:"false"`
	Key      ttls.FileOrContent `description:"The TLS key for Traefik Proxy as a TLS client." json:"key,omitempty" toml:"key,omitempty" yaml:"key,omitempty" loggable:"false"`
}

// Init the provider.
func (p *Provider) Init() error {
	return nil
}

// Provide allows the hub provider to provide configurations to traefik using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- dynamic.Message, _ *safe.Pool) error {
	if p.TLS == nil {
		return nil
	}

	listener, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		return fmt.Errorf("listener: %w", err)
	}

	port := listener.Addr().(*net.TCPAddr).Port

	client, err := createAgentClient(p.TLS)
	if err != nil {
		return fmt.Errorf("creating Hub Agent HTTP client: %w", err)
	}

	p.server = &http.Server{Handler: newHandler(APIEntrypoint, port, configurationChan, p.TLS, client)}

	// TODO: this is going to be leaky (because no context to make it terminate)
	// if/when Provide lifecycle differs with Traefik lifecycle.
	go func() {
		if err = p.server.Serve(listener); err != nil {
			log.WithoutContext().WithField(log.ProviderName, "hub").Errorf("Unexpected error while running server: %v", err)
			return
		}
	}()

	exposeAPIAndMetrics(configurationChan, APIEntrypoint, port, p.TLS)

	return nil
}

func exposeAPIAndMetrics(cfgChan chan<- dynamic.Message, ep string, port int, tlsCfg *TLS) {
	cfg := emptyDynamicConfiguration()

	patchDynamicConfiguration(cfg, ep, port, tlsCfg)

	cfgChan <- dynamic.Message{ProviderName: "hub", Configuration: cfg}
}

func patchDynamicConfiguration(cfg *dynamic.Configuration, ep string, port int, tlsCfg *TLS) {
	cfg.HTTP.Routers["traefik-hub-agent-api"] = &dynamic.Router{
		EntryPoints: []string{ep},
		Service:     "api@internal",
		Rule:        "Host(`proxy.traefik`) && PathPrefix(`/api`)",
	}
	cfg.HTTP.Routers["traefik-hub-agent-metrics"] = &dynamic.Router{
		EntryPoints: []string{ep},
		Service:     "prometheus@internal",
		Rule:        "Host(`proxy.traefik`) && PathPrefix(`/metrics`)",
	}

	cfg.HTTP.Routers["traefik-hub-agent-service"] = &dynamic.Router{
		EntryPoints: []string{ep},
		Service:     "traefik-hub-agent-service",
		Rule:        "Host(`proxy.traefik`) && PathPrefix(`/config`, `/discover-ip`, `/state`)",
	}

	cfg.HTTP.Services["traefik-hub-agent-service"] = &dynamic.Service{
		LoadBalancer: &dynamic.ServersLoadBalancer{
			Servers: []dynamic.Server{
				{
					URL: fmt.Sprintf("http://127.0.0.1:%d", port),
				},
			},
		},
	}

	if tlsCfg == nil {
		return
	}

	if tlsCfg.Insecure {
		cfg.TLS.Options["traefik-hub"] = ttls.Options{
			MinVersion: "VersionTLS13",
		}

		return
	}

	cfg.TLS.Options["traefik-hub"] = ttls.Options{
		ClientAuth: ttls.ClientAuth{
			CAFiles:        []ttls.FileOrContent{tlsCfg.CA},
			ClientAuthType: "RequireAndVerifyClientCert",
		},
		SniStrict:  true,
		MinVersion: "VersionTLS13",
	}

	cfg.TLS.Certificates = append(cfg.TLS.Certificates, &ttls.CertAndStores{
		Certificate: ttls.Certificate{
			CertFile: tlsCfg.Cert,
			KeyFile:  tlsCfg.Key,
		},
	})
}

func emptyDynamicConfiguration() *dynamic.Configuration {
	return &dynamic.Configuration{
		HTTP: &dynamic.HTTPConfiguration{
			Routers:           make(map[string]*dynamic.Router),
			Middlewares:       make(map[string]*dynamic.Middleware),
			Services:          make(map[string]*dynamic.Service),
			ServersTransports: make(map[string]*dynamic.ServersTransport),
		},
		TCP: &dynamic.TCPConfiguration{
			Routers:  make(map[string]*dynamic.TCPRouter),
			Services: make(map[string]*dynamic.TCPService),
		},
		TLS: &dynamic.TLSConfiguration{
			Stores:  make(map[string]ttls.Store),
			Options: make(map[string]ttls.Options),
		},
		UDP: &dynamic.UDPConfiguration{
			Routers:  make(map[string]*dynamic.UDPRouter),
			Services: make(map[string]*dynamic.UDPService),
		},
	}
}

func createAgentClient(tlsCfg *TLS) (http.Client, error) {
	var client http.Client
	if tlsCfg.Insecure {
		client.Transport = &http.Transport{
			TLSClientConfig: &tls.Config{
				InsecureSkipVerify: true,
				MinVersion:         tls.VersionTLS13,
			},
		}

		return client, nil
	}

	caContent, err := tlsCfg.CA.Read()
	if err != nil {
		return client, fmt.Errorf("reading CA: %w", err)
	}

	roots := x509.NewCertPool()
	if ok := roots.AppendCertsFromPEM(caContent); !ok {
		return client, errors.New("appending CA error")
	}

	certContent, err := tlsCfg.Cert.Read()
	if err != nil {
		return client, fmt.Errorf("reading Cert: %w", err)
	}
	keyContent, err := tlsCfg.Key.Read()
	if err != nil {
		return client, fmt.Errorf("reading Key: %w", err)
	}

	certificate, err := tls.X509KeyPair(certContent, keyContent)
	if err != nil {
		return client, fmt.Errorf("creating key pair: %w", err)
	}

	// mTLS
	client.Transport = &http.Transport{
		TLSClientConfig: &tls.Config{
			RootCAs:      roots,
			Certificates: []tls.Certificate{certificate},
			ServerName:   "agent.traefik",
			ClientAuth:   tls.RequireAndVerifyClientCert,
			MinVersion:   tls.VersionTLS13,
		},
	}

	return client, nil
}
