package tapo

import (
	"bufio"
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/md5"
	"crypto/sha256"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"net"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"github.com/AlexxIT/go2rtc/pkg/core"
	"github.com/AlexxIT/go2rtc/pkg/mpegts"
	"github.com/AlexxIT/go2rtc/pkg/tcp"
)

// Deprecated: should be rewritten to core.Connection
type Client struct {
	core.Listener

	url *url.URL

	medias    []*core.Media
	receivers []*core.Receiver
	sender    *core.Sender

	conn1 net.Conn
	conn2 net.Conn

	decrypt func(b []byte) []byte

	session1 string
	session2 string
	request  string

	recv int
	send int
}

// block ciphers using cipher block chaining.
type cbcMode interface {
	cipher.BlockMode
	SetIV([]byte)
}

// Dial support different urls:
//   - tapo://{cloud-password}@192.168.1.123 - auth to Tapo cameras
//     with cloud password (autodetect hash method)
//   - tapo://admin:{hashed-cloud-password}@192.168.1.123 - auth to Tapo cameras
//     with pre-hashed cloud password
//   - vigi://admin:{password}@192.168.1.123 - auth to Vigi cameras with password
//     for admin account (other not supported)
func Dial(rawURL string) (*Client, error) {
	u, err := url.Parse(rawURL)
	if err != nil {
		return nil, err
	}

	if u.Port() == "" {
		u.Host += ":8800"
	}

	c := &Client{url: u}
	if c.conn1, err = c.newConn(); err != nil {
		return nil, err
	}
	return c, nil
}

func (c *Client) newConn() (net.Conn, error) {
	req, err := http.NewRequest("POST", "http://"+c.url.Host+"/stream", nil)
	if err != nil {
		return nil, err
	}

	query := c.url.Query()

	if deviceId := query.Get("deviceId"); deviceId != "" {
		req.URL.RawQuery = "deviceId=" + deviceId
	}

	req.Header.Set("Content-Type", "multipart/mixed; boundary=--client-stream-boundary--")

	username := c.url.User.Username()
	password, _ := c.url.User.Password()

	conn, res, err := dial(req, c.url.Scheme, username, password)
	if err != nil {
		return nil, err
	}

	if res.StatusCode != http.StatusOK {
		return nil, errors.New(res.Status)
	}

	if c.decrypt == nil {
		c.newDectypter(res, c.url.Scheme, username, password)
	}

	channel := query.Get("channel")
	if channel == "" {
		channel = "0"
	}

	subtype := query.Get("subtype")
	switch subtype {
	case "", "0":
		subtype = "HD"
	case "1":
		subtype = "VGA"
	}

	c.request = fmt.Sprintf(
		`{"params":{"preview":{"audio":["default"],"channels":[%s],"resolutions":["%s"]},"method":"get"},"seq":1,"type":"request"}`,
		channel, subtype,
	)

	return conn, nil
}

func (c *Client) newDectypter(res *http.Response, brand, username, password string) {
	exchange := res.Header.Get("Key-Exchange")
	nonce := core.Between(exchange, `nonce="`, `"`)

	if brand == "tapo" && password == "" {
		if strings.Contains(exchange, `encrypt_type="3"`) {
			password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username)))
		} else {
			password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
		}
		username = "admin"
	}

	key := md5.Sum([]byte(nonce + ":" + password))
	iv := md5.Sum([]byte(username + ":" + nonce))

	block, err := aes.NewCipher(key[:])
	if err != nil {
		return
	}

	cbc := cipher.NewCBCDecrypter(block, iv[:]).(cbcMode)

	c.decrypt = func(b []byte) []byte {
		// restore IV
		cbc.SetIV(iv[:])

		// decrypt
		cbc.CryptBlocks(b, b)

		// unpad
		padSize := int(b[len(b)-1])
		return b[:len(b)-padSize]
	}
}

func (c *Client) SetupStream() (err error) {
	if c.session1 != "" {
		return
	}

	// audio: default, disable, enable
	c.session1, err = c.Request(c.conn1, []byte(c.request))
	return
}

// Handle - first run will be in probe state
func (c *Client) Handle() error {
	rd := multipart.NewReader(c.conn1, "--device-stream-boundary--")
	demux := mpegts.NewDemuxer()

	for {
		p, err := rd.NextRawPart()
		if err != nil {
			return err
		}

		if ct := p.Header.Get("Content-Type"); ct != "video/mp2t" {
			continue
		}

		cl := p.Header.Get("Content-Length")
		size, err := strconv.Atoi(cl)
		if err != nil {
			return err
		}

		c.recv += size

		body := make([]byte, size)

		b := body
		for {
			if n, err2 := p.Read(b); err2 == nil {
				b = b[n:]
			} else {
				break
			}
		}

		body = c.decrypt(body)
		bytesRd := bytes.NewReader(body)

		for {
			pkt, err2 := demux.ReadPacket(bytesRd)
			if pkt == nil || err2 == io.EOF {
				break
			}
			if err2 != nil {
				return err2
			}

			for _, receiver := range c.receivers {
				if receiver.ID == pkt.PayloadType {
					mpegts.TimestampToRTP(pkt, receiver.Codec)
					receiver.WriteRTP(pkt)
					break
				}
			}
		}
	}
}

func (c *Client) Close() (err error) {
	if c.conn1 != nil {
		err = c.conn1.Close()
	}
	if c.conn2 != nil {
		_ = c.conn2.Close()
	}
	return
}

func (c *Client) Request(conn net.Conn, body []byte) (string, error) {
	// TODO: fixme (size)
	buf := bytes.NewBuffer(nil)
	buf.WriteString("----client-stream-boundary--\r\n")
	buf.WriteString("Content-Type: application/json\r\n")
	buf.WriteString("Content-Length: " + strconv.Itoa(len(body)) + "\r\n\r\n")
	buf.Write(body)
	buf.WriteString("\r\n")

	if _, err := buf.WriteTo(conn); err != nil {
		return "", err
	}

	mpReader := multipart.NewReader(conn, "--device-stream-boundary--")

	for {
		p, err := mpReader.NextRawPart()
		if err != nil {
			return "", err
		}

		var v struct {
			Params struct {
				SessionID string `json:"session_id"`
			} `json:"params"`
		}

		if err = json.NewDecoder(p).Decode(&v); err != nil {
			return "", err
		}

		return v.Params.SessionID, nil
	}
}

func dial(req *http.Request, brand, username, password string) (net.Conn, *http.Response, error) {
	conn, err := net.DialTimeout("tcp", req.URL.Host, core.ConnDialTimeout)
	if err != nil {
		return nil, nil, err
	}

	if err = req.Write(conn); err != nil {
		return nil, nil, err
	}

	r := bufio.NewReader(conn)

	res, err := http.ReadResponse(r, req)
	if err != nil {
		return nil, nil, err
	}
	_, _ = io.Copy(io.Discard, res.Body) // discard leftovers
	_ = res.Body.Close() // ignore response body

	auth := res.Header.Get("WWW-Authenticate")

	if res.StatusCode != http.StatusUnauthorized || !strings.HasPrefix(auth, "Digest") {
		return nil, nil, fmt.Errorf("Expected StatusCode to be %d, received %d", http.StatusUnauthorized, res.StatusCode)
	}

	if brand == "tapo" && password == "" {
		// support cloud password in place of username
		if strings.Contains(auth, `encrypt_type="3"`) {
			password = fmt.Sprintf("%32X", sha256.Sum256([]byte(username)))
		} else {
			password = fmt.Sprintf("%16X", md5.Sum([]byte(username)))
		}
		username = "admin"
	} else if brand == "vigi" && username == "admin" {
		password = securityEncode(password)
	}

	realm := tcp.Between(auth, `realm="`, `"`)
	nonce := tcp.Between(auth, `nonce="`, `"`)
	qop := tcp.Between(auth, `qop="`, `"`)
	uri := req.URL.RequestURI()
	ha1 := tcp.HexMD5(username, realm, password)
	ha2 := tcp.HexMD5(req.Method, uri)
	nc := "00000001"
	cnonce := core.RandString(32, 64)
	response := tcp.HexMD5(ha1, nonce, nc, cnonce, qop, ha2)

	// https://datatracker.ietf.org/doc/html/rfc7616
	header := fmt.Sprintf(
		`Digest username="%s", realm="%s", nonce="%s", uri="%s", qop=%s, nc=%s, cnonce="%s", response="%s"`,
		username, realm, nonce, uri, qop, nc, cnonce, response,
	)

	if opaque := tcp.Between(auth, `opaque="`, `"`); opaque != "" {
		header += fmt.Sprintf(`, opaque="%s", algorithm=MD5`, opaque)
	}

	req.Header.Set("Authorization", header)

	if err = req.Write(conn); err != nil {
		return nil, nil, err
	}

	if res, err = http.ReadResponse(r, req); err != nil {
		return nil, nil, err
	}

	return conn, res, nil
}

const (
	keyShort = "RDpbLfCPsJZ7fiv"
	keyLong  = "yLwVl0zKqws7LgKPRQ84Mdt708T1qQ3Ha7xv3H7NyU84p21BriUWBU43odz3iP4rBL3cD02KZciXTysVXiV8ngg6vL48rPJyAUw0HurW20xqxv9aYb4M9wK1Ae0wlro510qXeU07kV57fQMc8L6aLgMLwygtc0F10a0Dg70TOoouyFhdysuRMO51yY5ZlOZZLEal1h0t9YQW0Ko7oBwmCAHoic4HYbUyVeU3sfQ1xtXcPcf1aT303wAQhv66qzW"
)

func securityEncode(s string) string {
	size := len(s)

	var n int // max
	if size > len(keyShort) {
		n = size
	} else {
		n = len(keyShort)
	}

	b := make([]byte, n)

	for i := 0; i < n; i++ {
		c1 := 187
		c2 := 187
		if i >= size {
			c1 = int(keyShort[i])
		} else if i >= len(keyShort) {
			c2 = int(s[i])
		} else {
			c1 = int(keyShort[i])
			c2 = int(s[i])
		}
		b[i] = keyLong[(c1^c2)%len(keyLong)]
	}

	return string(b)
}
