package hap

import (
	"bufio"
	"bytes"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
	"github.com/AlexxIT/go2rtc/pkg/hap/curve25519"
	"github.com/AlexxIT/go2rtc/pkg/hap/ed25519"
	"github.com/AlexxIT/go2rtc/pkg/hap/hkdf"
	"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
	"github.com/AlexxIT/go2rtc/pkg/mdns"
)

const (
	ConnDialTimeout = time.Second * 3
	ConnDeadline    = time.Second * 3
)

// Client for HomeKit. DevicePublic can be null.
type Client struct {
	DeviceAddress string // including port
	DeviceID      string // aka. Accessory
	DevicePublic  []byte
	ClientID      string // aka. Controller
	ClientPrivate []byte

	OnEvent func(res *http.Response)
	//Output  func(msg any)

	Conn   net.Conn
	reader *bufio.Reader

	res chan *http.Response
	err error
}

func Dial(rawURL string) (*Client, error) {
	u, err := url.Parse(rawURL)
	if err != nil {
		return nil, err
	}

	query := u.Query()
	c := &Client{
		DeviceAddress: u.Host,
		DeviceID:      query.Get("device_id"),
		DevicePublic:  DecodeKey(query.Get("device_public")),
		ClientID:      query.Get("client_id"),
		ClientPrivate: DecodeKey(query.Get("client_private")),
	}

	if err = c.Dial(); err != nil {
		return nil, err
	}

	return c, nil
}

func (c *Client) ClientPublic() []byte {
	return c.ClientPrivate[32:]
}

func (c *Client) URL() string {
	return fmt.Sprintf(
		"homekit://%s?device_id=%s&device_public=%16x&client_id=%s&client_private=%32x",
		c.DeviceAddress, c.DeviceID, c.DevicePublic, c.ClientID, c.ClientPrivate,
	)
}

func (c *Client) DeviceHost() string {
	if i := strings.IndexByte(c.DeviceAddress, ':'); i > 0 {
		return c.DeviceAddress[:i]
	}
	return c.DeviceAddress
}

func (c *Client) Dial() (err error) {
	if len(c.ClientID) == 0 || len(c.ClientPrivate) == 0 {
		return errors.New("hap: can't dial witout client_id or client_private")
	}

	// update device address (host and/or port) before dial
	_ = mdns.QueryOrDiscovery(c.DeviceHost(), mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
		if entry.Complete() && entry.Info["id"] == c.DeviceID {
			c.DeviceAddress = entry.Addr()
			return true
		}
		return false
	})

	// TODO: close conn on error
	if c.Conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout); err != nil {
		return
	}

	c.reader = bufio.NewReader(c.Conn)

	// STEP M1: send our session public to device
	sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()

	// 1. Send sessionPublic
	plainM1 := struct {
		PublicKey string `tlv8:"3"`
		State     byte   `tlv8:"6"`
	}{
		PublicKey: string(sessionPublic),
		State:     StateM1,
	}
	res, err := c.Post(PathPairVerify, MimeTLV8, tlv8.MarshalReader(plainM1))
	if err != nil {
		return
	}

	// STEP M2: unpack deviceID from response
	var cipherM2 struct {
		PublicKey     string `tlv8:"3"`
		EncryptedData string `tlv8:"5"`
		State         byte   `tlv8:"6"`
	}
	if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &cipherM2); err != nil {
		return err
	}
	if cipherM2.State != StateM2 {
		return newResponseError(plainM1, cipherM2)
	}

	// 1. generate session shared key
	sessionShared, err := curve25519.SharedSecret(sessionPrivate, []byte(cipherM2.PublicKey))
	if err != nil {
		return
	}

	sessionKey, err := hkdf.Sha512(
		sessionShared, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info",
	)
	if err != nil {
		return
	}

	// 2. decrypt M2 response with session key
	b, err := chacha20poly1305.Decrypt(sessionKey, "PV-Msg02", []byte(cipherM2.EncryptedData))
	if err != nil {
		return
	}

	// 3. unpack payload from TLV8
	var plainM2 struct {
		Identifier string `tlv8:"1"`
		Signature  string `tlv8:"10"`
	}
	if err = tlv8.Unmarshal(b, &plainM2); err != nil {
		return
	}

	// 4. verify signature for M2 response with device public
	// device session + device id + our session
	if c.DevicePublic != nil {
		b = Append(cipherM2.PublicKey, plainM2.Identifier, sessionPublic)
		if !ed25519.ValidateSignature(c.DevicePublic, b, []byte(plainM2.Signature)) {
			return errors.New("hap: ValidateSignature")
		}
	}

	// STEP M3: send our clientID to device
	// 1. generate signature with our private key
	// (our session + our ID + device session)
	b = Append(sessionPublic, c.ClientID, cipherM2.PublicKey)
	if b, err = ed25519.Signature(c.ClientPrivate, b); err != nil {
		return
	}

	// 2. generate payload
	plainM3 := struct {
		Identifier string `tlv8:"1"`
		Signature  string `tlv8:"10"`
	}{
		Identifier: c.ClientID,
		Signature:  string(b),
	}
	if b, err = tlv8.Marshal(plainM3); err != nil {
		return
	}

	// 4. encrypt payload with session key
	if b, err = chacha20poly1305.Encrypt(sessionKey, "PV-Msg03", b); err != nil {
		return
	}

	// 4. generate request
	cipherM3 := struct {
		EncryptedData string `tlv8:"5"`
		State         byte   `tlv8:"6"`
	}{
		State:         StateM3,
		EncryptedData: string(b),
	}
	if res, err = c.Post(PathPairVerify, MimeTLV8, tlv8.MarshalReader(cipherM3)); err != nil {
		return
	}

	// STEP M4. Read response
	var plainM4 struct {
		State byte `tlv8:"6"`
	}
	if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM4); err != nil {
		return
	}
	if plainM4.State != StateM4 {
		return newResponseError(cipherM3, plainM4)
	}

	rw := bufio.NewReadWriter(c.reader, bufio.NewWriter(c.Conn))

	// like tls.Client wrapper over net.Conn
	if c.Conn, err = NewConn(c.Conn, rw, sessionShared, true); err != nil {
		return
	}
	// new reader for new conn
	c.reader = bufio.NewReader(c.Conn)

	return
}

func (c *Client) Close() error {
	if c.Conn == nil {
		return nil
	}
	return c.Conn.Close()
}

func (c *Client) eventsReader() {
	c.res = make(chan *http.Response)

	for {
		var res *http.Response
		if res, c.err = ReadResponse(c.reader, nil); c.err != nil {
			break
		}

		var body []byte
		if body, c.err = io.ReadAll(res.Body); c.err != nil {
			break
		}

		res.Body = io.NopCloser(bytes.NewReader(body))

		if res.Proto != ProtoEvent {
			c.res <- res
		} else if c.OnEvent != nil {
			c.OnEvent(res)
		}
	}

	close(c.res)
}

func (c *Client) GetAccessories() ([]*Accessory, error) {
	res, err := c.Get(PathAccessories)
	if err != nil {
		return nil, err
	}

	var v JSONAccessories
	if err = json.NewDecoder(res.Body).Decode(&v); err != nil {
		return nil, err
	}

	return v.Value, nil
}

func (c *Client) GetFirstAccessory() (*Accessory, error) {
	accs, err := c.GetAccessories()
	if err != nil {
		return nil, err
	}
	if len(accs) == 0 {
		return nil, errors.New("hap: GetAccessories zero answer")
	}
	return accs[0], nil
}

func (c *Client) GetCharacters(query string) ([]JSONCharacter, error) {
	res, err := c.Get(PathCharacteristics + "?id=" + query)
	if err != nil {
		return nil, err
	}

	data, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}

	var v JSONCharacters
	if err = json.Unmarshal(data, &v); err != nil {
		return nil, err
	}
	return v.Value, nil
}

func (c *Client) GetCharacter(char *Character) error {
	query := fmt.Sprintf("%d.%d", DeviceAID, char.IID)
	chars, err := c.GetCharacters(query)
	if err != nil {
		return err
	}
	char.Value = chars[0].Value
	return nil
}

func (c *Client) PutCharacters(characters ...*Character) error {
	var v JSONCharacters
	for i, char := range characters {
		v.Value = append(v.Value, JSONCharacter{
			AID:   1,
			IID:   char.IID,
			Value: char.Value,
		})
		characters[i] = char
	}
	body, err := json.Marshal(v)
	if err != nil {
		return err
	}

	res, err := c.Put(PathCharacteristics, MimeJSON, bytes.NewReader(body))
	if err != nil {
		return err
	}

	_, _ = io.ReadAll(res.Body) // important to "clear" body

	return nil
}

func (c *Client) GetImage(width, height int) ([]byte, error) {
	s := fmt.Sprintf(
		`{"image-width":%d,"image-height":%d,"resource-type":"image","reason":0}`,
		width, height,
	)
	res, err := c.Post(PathResource, MimeJSON, bytes.NewBufferString(s))
	if err != nil {
		return nil, err
	}
	return io.ReadAll(res.Body)
}

func (c *Client) LocalIP() string {
	if c.Conn == nil {
		return ""
	}
	addr := c.Conn.LocalAddr().(*net.TCPAddr)
	return addr.IP.String()
}

func DecodeKey(s string) []byte {
	if s == "" {
		return nil
	}
	data, err := hex.DecodeString(s)
	if err != nil {
		return nil
	}
	return data
}
