// Copyright © 2016 Asteris, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package port

import (
	"fmt"
	"net"

	log "github.com/Sirupsen/logrus"
	"github.com/asteris-llc/converge/resource"
	"github.com/asteris-llc/converge/resource/wait"
	"golang.org/x/net/context"
)

// Port represents a port check
type Port struct {
	*wait.Retrier

	// the hostname
	Host string `export:"host"`

	// the TCP port
	Port int `export:"port"`

	ConnectionCheck
}

// Check if the port is open
func (p *Port) Check(context.Context, resource.Renderer) (resource.TaskStatus, error) {
	status := resource.NewStatus()

	err := p.CheckConnection()
	if err == nil {
		if p.RetryCount > 0 {
			status.AddMessage(fmt.Sprintf("Passed after %d retries (%v)", p.RetryCount, p.Duration))
		}
	} else {
		// The desired state is that the port will be available after the apply. We
		// also want to indicate that the port is not available in the plan output.
		// Therefore, we set the status to StatusWillChange.
		status.RaiseLevel(resource.StatusWillChange)
		status.AddMessage(fmt.Sprintf("Failed to connect to %s:%d: %s", p.Host, p.Port, err.Error()))
		if p.RetryCount > 0 { // only add retry messages after an apply attempt
			status.AddMessage(fmt.Sprintf("Failed after %d retries (%v)", p.RetryCount, p.Duration))
		}
	}

	return status, nil
}

// Apply retries the check until it passes or returns max failure threshold
func (p *Port) Apply(context.Context) (resource.TaskStatus, error) {
	status := resource.NewStatus()

	_, err := p.RetryUntil(func() (bool, error) {
		checkErr := p.CheckConnection()
		// CheckConnection returns an err if the connection fails but RetryUntil
		// breaks on errors. So if we get an error, we just return false and
		// otherwise ignore the error
		return checkErr == nil, nil
	})

	return status, err
}

// CheckConnection attempts to see if a tcp port is open
func (p *Port) CheckConnection() error {
	if p.ConnectionCheck == nil {
		p.ConnectionCheck = &TCPConnectionCheck{}
	}
	return p.ConnectionCheck.CheckConnection(p.Host, p.Port)
}

// ConnectionCheck represents a connection checker
type ConnectionCheck interface {
	CheckConnection(host string, port int) error
}

// TCPConnectionCheck impelements a ConnectionCheck over TCP
type TCPConnectionCheck struct{}

// CheckConnection attempts to see if a tcp port is open
func (t *TCPConnectionCheck) CheckConnection(host string, port int) error {
	logger := log.WithField("module", "wait.port")

	addr := fmt.Sprintf("%s:%d", host, port)
	conn, err := net.Dial("tcp", addr)
	if err != nil {
		logger.WithError(err).WithField("addr", addr).Debug("connection failed")
		return err
	}
	defer conn.Close()

	return nil
}
