package xmpp

import (
	"context"
	"crypto/sha1"
	"encoding/hex"
	"encoding/xml"
	"errors"
	"fmt"
	"gosrc.io/xmpp/stanza"
	"io"
)

type ComponentOptions struct {
	TransportConfiguration

	// =================================
	// Component Connection Info

	// Domain is the XMPP server subdomain that the component will handle
	Domain string
	// Secret is the "password" used by the XMPP server to secure component access
	Secret string

	// =================================
	// Component discovery

	// Component human readable name, that will be shown in XMPP discovery
	Name string
	// Typical categories and types: https://xmpp.org/registrar/disco-categories.html
	Category string
	Type     string

	// =================================
	// Communication with developer client / StreamManager

	// Track and broadcast connection state
	EventManager
}

// Component implements an XMPP extension allowing to extend XMPP server
// using external components. Component specifications are defined
// in XEP-0114, XEP-0355 and XEP-0356.
type Component struct {
	ComponentOptions
	router *Router

	transport Transport

	// read / write
	socketProxy  io.ReadWriter // TODO
	ErrorHandler func(error)
}

func NewComponent(opts ComponentOptions, r *Router, errorHandler func(error)) (*Component, error) {
	c := Component{ComponentOptions: opts, router: r, ErrorHandler: errorHandler}
	return &c, nil
}

// Connect triggers component connection to XMPP server component port.
// TODO: Failed handshake should be a permanent error
func (c *Component) Connect() error {
	var state SMState
	return c.Resume(state)
}

func (c *Component) Resume(sm SMState) error {
	var err error
	var streamId string
	if c.ComponentOptions.TransportConfiguration.Domain == "" {
		c.ComponentOptions.TransportConfiguration.Domain = c.ComponentOptions.Domain
	}
	c.transport, err = NewComponentTransport(c.ComponentOptions.TransportConfiguration)
	if err != nil {
		c.updateState(StatePermanentError)

		return NewConnError(err, true)
	}

	if streamId, err = c.transport.Connect(); err != nil {
		c.updateState(StatePermanentError)

		return NewConnError(err, true)
	}
	c.updateState(StateConnected)

	// Authentication
	if err := c.sendWithWriter(c.transport, []byte(fmt.Sprintf("<handshake>%s</handshake>", c.handshake(streamId)))); err != nil {
		c.updateState(StateStreamError)

		return NewConnError(errors.New("cannot send handshake "+err.Error()), false)
	}

	// Check server response for authentication
	val, err := stanza.NextPacket(c.transport.GetDecoder())
	if err != nil {
		c.updateState(StatePermanentError)
		return NewConnError(err, true)
	}

	switch v := val.(type) {
	case stanza.StreamError:
		c.streamError("conflict", "no auth loop")
		return NewConnError(errors.New("handshake failed "+v.Error.Local), true)
	case stanza.Handshake:
		// Start the receiver go routine
		c.updateState(StateSessionEstablished)
		go c.recv()
		return err // Should be empty at this point
	default:
		c.updateState(StatePermanentError)
		return NewConnError(errors.New("expecting handshake result, got "+v.Name()), true)
	}
}

func (c *Component) Disconnect() error {
	// TODO: Add a way to wait for stream close acknowledgement from the server for clean disconnect
	if c.transport != nil {
		return c.transport.Close()
	}
	// No transport so no connection.
	return nil
}

func (c *Component) SetHandler(handler EventHandler) {
	c.Handler = handler
}

// Receiver Go routine receiver
func (c *Component) recv() {
	for {
		val, err := stanza.NextPacket(c.transport.GetDecoder())
		if err != nil {
			c.updateState(StateDisconnected)
			c.ErrorHandler(err)
			return
		}
		// Handle stream errors
		switch p := val.(type) {
		case stanza.StreamError:
			c.router.route(c, val)
			c.streamError(p.Error.Local, p.Text)
			c.ErrorHandler(errors.New("stream error: " + p.Error.Local))
			// We don't return here, because we want to wait for the stream close tag from the server, or timeout.
			c.Disconnect()
		case stanza.StreamClosePacket:
			// TCP messages should arrive in order, so we can expect to get nothing more after this occurs
			c.transport.ReceivedStreamClose()
			return
		}
		c.router.route(c, val)
	}
}

// Send marshalls XMPP stanza and sends it to the server.
func (c *Component) Send(packet stanza.Packet) error {
	transport := c.transport
	if transport == nil {
		return errors.New("component is not connected")
	}

	data, err := xml.Marshal(packet)
	if err != nil {
		return errors.New("cannot marshal packet " + err.Error())
	}

	if err := c.sendWithWriter(transport, data); err != nil {
		return errors.New("cannot send packet " + err.Error())
	}
	return nil
}

func (c *Component) sendWithWriter(writer io.Writer, packet []byte) error {
	var err error
	_, err = writer.Write(packet)
	return err
}

// SendIQ sends an IQ set or get stanza to the server. If a result is received
// the provided handler function will automatically be called.
//
// The provided context should have a timeout to prevent the client from waiting
// forever for an IQ result. For example:
//
//   ctx, _ := context.WithTimeout(context.Background(), 30 * time.Second)
//   result := <- client.SendIQ(ctx, iq)
//
func (c *Component) SendIQ(ctx context.Context, iq stanza.IQ) (chan stanza.IQ, error) {
	if iq.Attrs.Type != stanza.IQTypeSet && iq.Attrs.Type != stanza.IQTypeGet {
		return nil, ErrCanOnlySendGetOrSetIq
	}
	if err := c.Send(iq); err != nil {
		return nil, err
	}
	return c.router.NewIQResultRoute(ctx, iq.Attrs.Id), nil
}

// SendRaw sends an XMPP stanza as a string to the server.
// It can be invalid XML or XMPP content. In that case, the server will
// disconnect the component. It is up to the user of this method to
// carefully craft the XML content to produce valid XMPP.
func (c *Component) SendRaw(packet string) error {
	transport := c.transport
	if transport == nil {
		return errors.New("component is not connected")
	}

	var err error
	err = c.sendWithWriter(transport, []byte(packet))
	return err
}

// handshake generates an authentication token based on StreamID and shared secret.
func (c *Component) handshake(streamId string) string {
	// 1. Concatenate the Stream ID received from the server with the shared secret.
	concatStr := streamId + c.Secret

	// 2. Hash the concatenated string according to the SHA1 algorithm, i.e., SHA1( concat (sid, password)).
	h := sha1.New()
	h.Write([]byte(concatStr))
	hash := h.Sum(nil)

	// 3. Ensure that the hash output is in hexadecimal format, not binary or base64.
	// 4. Convert the hash output to all lowercase characters.
	encodedStr := hex.EncodeToString(hash)

	return encodedStr
}

/*
TODO: Add support for discovery management directly in component
TODO: Support multiple identities on disco info
TODO: Support returning features on disco info
*/