package service

import "os"
import "log"
import "fmt"
import "net"
import "sync"
import "bufio"
import "errors"
import "strings"
import "crypto/tls"
import "encoding/base64"
import "hnakra/protocol"

// Mount is an interface satisfied by all mount types.
type Mount interface {
	Run      (ServiceInfo) error
	Close    () error
	Shutdown () error
}

// MountInfo contains information about a mount point.
type MountInfo struct {
	// Host specifies the host to mount on. If the host is left empty, it
	// will default to @ (meaning default/any host). The port is entirely up
	// to the router.
	// Maximum length: 255 bytes
	Host string

	// Scheme specifies the protocol to mount on. This will be automatically
	// set by specialized mount types, so setting it manually shouldn't be
	// needed.
	// Maximum length: 255 bytes
	Scheme string

	// Path specifies the path to mount on. If the path ends with a /, then
	// all requests under the path will be sent to this service. If there is
	// no trailing /, this service will only recieve requests that match the
	// path exactly (when normalized).
	// Maximum length: 2^16-1 bytes
	Path string
}

// String returns a string representation of the mount.
func (mount *MountInfo) String () string {
	return mount.Scheme + "://" + mount.Host + mount.Path
}

// FillDefault fills most empty fields with a hard-coded default value.
func (mount *MountInfo) FillDefault () {
	if mount.Host == "" { mount.Host   = "@" }
	if mount.Path == "" { mount.Scheme = "/" }
}

// Fits returns an error if any data is too big to send over the connection.
func (mount *MountInfo) Fits () error {
	switch {
	case len(mount.Host) > 255:
		return errors.New("host cannot be longer than 255 bytes")
	case len(mount.Scheme) > 255:
		return errors.New("scheme cannot be longer than 255 bytes")
	case len(mount.Path) > int(protocol.MaxIntOfSize(2)):
		return errors.New(fmt.Sprint (
			"mount point path cannot be longer than ",
			protocol.MaxIntOfSize(2), " bytes"))
	default: return nil
	}
}

// ServiceInfo contains information about the service as a whole, such as a
// human readable description and login credentials.
type ServiceInfo struct {
	// Router specifies the host:port of the router to connect to. This
	// defaults to $HNAKRA_ROUTER_HOST:$HNAKRA_ROUTER_PORT if left empty.
	// The default value of these environment variables (if not set) is
	// localhost:2048. If you are giving this service to other people, it is
	// a good idea to leave this empty.
	Router string

	// Name should be set to a human-readable name of the service.
	// Maximum length: 255 bytes
	Name string

	// Description should be set to a human-readable description of what the
	// service does.
	// Maximum length: 255 bytes
	Description string

	// User is an optional string that specifies who is connecting to the
	// router. This can be used by routers in conjunction with the key
	// bytes in order to enforce rules about which service can do what. This
	// defaults to $HNAKRA_USER if left empty. If you are giving this
	// service to other people, it is a good idea to leave this empty.
	// Maximum length: 255 bytes
	User string

	// Key is an optional byte slice that is sent to the router in order for
	// it to authorize the connection. If nil, this defaults to the contents
	// of $HNAKRA_KEY interpreted as base64. Do not embed a key in your
	// code - you should only use this if your service reads the key from a
	// safe source upon startup. In addition to this, If you choose to set
	// the key via the aforementioned enviroment variable, ensure that it
	// can only be read by the service.
	// Maximum length: 255 bytes
	Key []byte

	// TLSConfig is an optional TLS configuration. If you are looking to
	// set InsecureSkipVerify to false, consider instead setting the
	// environment variables $SSL_CERT_FILE or $SSL_CERT_DIR to point toward
	// a custom root certificate.
	TLSConfig *tls.Config	
}

// FillDefault fills most empty fields with values from environment variables.
// If an environment variable is blank, it uses a hard-coded default value
// instead.
func (service *ServiceInfo) FillDefault () (err error) {
	// host
	defaultRouterHost := os.Getenv("HNAKRA_ROUTER_HOST")
	if defaultRouterHost == "" {
		defaultRouterHost = "localhost"
	}
	defaultRouterPort := os.Getenv("HNAKRA_ROUTER_PORT")
	if defaultRouterPort == "" {
		defaultRouterPort = "2048"
	}
	routerHost, routerPort, _ := strings.Cut(service.Router, ":")
	if routerHost == "" {
		routerHost = defaultRouterHost
	}
	if routerPort == "" {
		routerPort = defaultRouterPort
	}
	service.Router = routerHost + ":" + routerPort

	// user
	if service.User == "" {
		service.User = os.Getenv("HNAKRA_USER")
	}

	// key
	if service.Key == nil {
		base64Key := os.Getenv("HNAKRA_KEY")
		service.Key, err = base64.StdEncoding.DecodeString(base64Key)
		if err != nil { return }
	}

	return
}

// Fits returns an error if any data is too big to send over the connection.
func (service *ServiceInfo) Fits () (err error) {
	switch {
	case len(service.Name) > 255:
		return errors.New("name cannot be longer than 255 bytes")
	case len(service.Description) > 255:
		return errors.New("description cannot be longer than 255 bytes")
	case len(service.User) > 255:
		return errors.New("user cannot be longer than 255 bytes")
	case len(service.Key) > 255:
		return errors.New("key cannot be longer than 255 bytes")
	default: return nil
	}
}

// Conn represents a connection to a router.
type Conn struct {
	IDFactory *protocol.IDFactory
	
	conn       net.Conn
	writeLock  sync.Mutex
	readWriter *bufio.ReadWriter
}

// Dial connects to a router, returning the resulting connection. It handles
// performing the login sequence and sets ID(0) as active automatically.
func Dial (mount MountInfo, service ServiceInfo) (conn *Conn, err error) {
	// fill in default values from env variables and such
	mount.FillDefault()
	err = service.FillDefault()
	if err != nil { return nil, err }

	// sanity check
	err = mount.Fits()
	if err != nil { return nil, err }
	err = service.Fits()
	if err != nil { return nil, err }

	conn = &Conn {
		IDFactory: protocol.NewServiceIDFactory(),
	}

	// connect to router
	log.Println("... dialing", service.Router)
	conn.conn, err = tls.Dial("tcp", service.Router, service.TLSConfig)
	if err != nil { return nil, err }
	conn.readWriter = bufio.NewReadWriter (
		bufio.NewReader(conn.conn),
		bufio.NewWriter(conn.conn))
	
	// log in
	log.Println("... logging in as", service.User, "on", mount)
	err = conn.Send(protocol.MessageLogin {
		ID:          conn.IDFactory.Next(),
		Version:     protocol.Version { Major: 0, Minor: 0 },
		User:        service.User,
		Key:         service.Key,
		Name:        service.Name,
		Description: service.Description,
		Scheme:      mount.Scheme,
		Host:        mount.Host,
		Path:        mount.Path,
	})
	if err != nil {
		conn.Close()
		return nil, err
	}

	// read status
	message, err := conn.Receive()
	if err != nil {
		conn.Close()
		return nil, err
	}
	status, ok := message.(protocol.MessageStatus)
	if !ok {
		conn.Close()
		return nil, errors.New(fmt.Sprint (
			"router sent unknown type, expecting",
			protocol.TypeStatus))
	}
	if status.Status != protocol.StatusOk {
		return nil, status
	}
	
	log.Println(".// logged in")
	return conn, nil
}

// Send sends a message along the connection, along with its type code. This
// method may be called concurrently.
func (conn *Conn) Send (message protocol.Message) (err error) {
	conn.writeLock.Lock()
	defer conn.writeLock.Unlock()
	err = message.Send(conn.readWriter)
	if err != nil { return }
	return conn.readWriter.Flush()
}

// Receive recieves a message from the connection. This method may not be called
// concurrently.
func (conn *Conn) Receive () (message protocol.Message, err error) {
	return protocol.ReadMessage(conn.conn)
}

// Close closes the connection.
func (conn *Conn) Close () error {
	return conn.conn.Close()
}
