package wyze

import (
	"crypto/rand"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"net"
	"net/url"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/AlexxIT/go2rtc/pkg/tutk"
	"github.com/AlexxIT/go2rtc/pkg/tutk/dtls"
)

const (
	FrameSize1080P      = 0
	FrameSize360P       = 1
	FrameSize720P       = 2
	FrameSize2K         = 3
	FrameSizeFloodlight = 4
)

const (
	BitrateMax uint16 = 0xF0
	BitrateSD  uint16 = 0x3C
)

const (
	MediaTypeVideo       = 1
	MediaTypeAudio       = 2
	MediaTypeReturnAudio = 3
	MediaTypeRDT         = 4
)

const (
	KCmdAuth               = 10000
	KCmdChallenge          = 10001
	KCmdChallengeResp      = 10002
	KCmdAuthResult         = 10003
	KCmdControlChannel     = 10010
	KCmdControlChannelResp = 10011
	KCmdSetResolutionDB    = 10052
	KCmdSetResolutionDBRes = 10053
	KCmdSetResolution      = 10056
	KCmdSetResolutionResp  = 10057
)

type Client struct {
	conn *dtls.DTLSConn

	host  string
	uid   string
	enr   string
	mac   string
	model string

	authKey string
	verbose bool

	closed  bool
	closeMu sync.Mutex

	hasAudio    bool
	hasIntercom bool

	audioCodecID    byte
	audioSampleRate uint32
	audioChannels   uint8
}

type AuthResponse struct {
	ConnectionRes string         `json:"connectionRes"`
	CameraInfo    map[string]any `json:"cameraInfo"`
}

func Dial(rawURL string) (*Client, error) {
	u, err := url.Parse(rawURL)
	if err != nil {
		return nil, fmt.Errorf("wyze: invalid URL: %w", err)
	}

	query := u.Query()

	if query.Get("dtls") != "true" {
		return nil, fmt.Errorf("wyze: only DTLS cameras are supported")
	}

	c := &Client{
		host:    u.Host,
		uid:     query.Get("uid"),
		enr:     query.Get("enr"),
		mac:     query.Get("mac"),
		model:   query.Get("model"),
		verbose: query.Get("verbose") == "true",
	}

	c.authKey = string(dtls.CalculateAuthKey(c.enr, c.mac))

	if c.verbose {
		fmt.Printf("[Wyze] Connecting to %s (UID: %s)\n", c.host, c.uid)
	}

	if err := c.connect(); err != nil {
		c.Close()
		return nil, err
	}

	if err := c.doAVLogin(); err != nil {
		c.Close()
		return nil, err
	}

	if err := c.doKAuth(); err != nil {
		c.Close()
		return nil, err
	}

	if c.verbose {
		fmt.Printf("[Wyze] Connection established\n")
	}

	return c, nil
}

func (c *Client) SupportsAudio() bool {
	return c.hasAudio
}

func (c *Client) SupportsIntercom() bool {
	return c.hasIntercom
}

func (c *Client) SetBackchannelCodec(codecID byte, sampleRate uint32, channels uint8) {
	c.audioCodecID = codecID
	c.audioSampleRate = sampleRate
	c.audioChannels = channels
}

func (c *Client) GetBackchannelCodec() (codecID byte, sampleRate uint32, channels uint8) {
	return c.audioCodecID, c.audioSampleRate, c.audioChannels
}

func (c *Client) SetResolution(quality byte) error {
	var frameSize uint8
	var bitrate uint16

	switch quality {
	case 0: // Auto/HD - use model's best
		frameSize = c.hdFrameSize()
		bitrate = BitrateMax
	case FrameSize360P: // 1 = SD/360P
		frameSize = FrameSize360P
		bitrate = BitrateSD
	case FrameSize720P: // 2 = 720P
		frameSize = FrameSize720P
		bitrate = BitrateMax
	case FrameSize2K: // 3 = 2K
		if c.is2K() {
			frameSize = FrameSize2K
		} else {
			frameSize = c.hdFrameSize()
		}
		bitrate = BitrateMax
	case FrameSizeFloodlight: // 4 = Floodlight
		frameSize = c.hdFrameSize()
		bitrate = BitrateMax
	default:
		frameSize = quality
		bitrate = BitrateMax
	}

	if c.verbose {
		fmt.Printf("[Wyze] SetResolution: quality=%d frameSize=%d bitrate=%d model=%s\n", quality, frameSize, bitrate, c.model)
	}

	// Use K10052 (doorbell format) for certain models
	if c.useDoorbellResolution() {
		k10052 := c.buildK10052(frameSize, bitrate)
		_, err := c.conn.WriteAndWaitIOCtrl(k10052, c.matchHL(KCmdSetResolutionDBRes), 5*time.Second)
		return err
	}

	k10056 := c.buildK10056(frameSize, bitrate)
	_, err := c.conn.WriteAndWaitIOCtrl(k10056, c.matchHL(KCmdSetResolutionResp), 5*time.Second)
	return err
}

func (c *Client) StartVideo() error {
	k10010 := c.buildK10010(MediaTypeVideo, true)
	_, err := c.conn.WriteAndWaitIOCtrl(k10010, c.matchHL(KCmdControlChannelResp), 5*time.Second)
	return err
}

func (c *Client) StartAudio() error {
	k10010 := c.buildK10010(MediaTypeAudio, true)
	_, err := c.conn.WriteAndWaitIOCtrl(k10010, c.matchHL(KCmdControlChannelResp), 5*time.Second)
	return err
}

func (c *Client) StartIntercom() error {
	if c.conn == nil {
		return fmt.Errorf("connection is nil")
	}

	if c.conn.IsBackchannelReady() {
		return nil
	}

	k10010 := c.buildK10010(MediaTypeReturnAudio, true)
	if _, err := c.conn.WriteAndWaitIOCtrl(k10010, c.matchHL(KCmdControlChannelResp), 5*time.Second); err != nil {
		return fmt.Errorf("enable return audio: %w", err)
	}

	if c.verbose {
		fmt.Printf("[Wyze] Speaker channel enabled, waiting for readiness...\n")
	}

	return c.conn.AVServStart()
}

func (c *Client) StopIntercom() error {
	if c.conn == nil || !c.conn.IsBackchannelReady() {
		return nil
	}

	k10010 := c.buildK10010(MediaTypeReturnAudio, false)
	c.conn.WriteIOCtrl(k10010)

	return c.conn.AVServStop()
}

func (c *Client) ReadPacket() (*tutk.Packet, error) {
	return c.conn.AVRecvFrameData()
}

func (c *Client) WriteAudio(codec byte, payload []byte, timestamp uint32, sampleRate uint32, channels uint8) error {
	if !c.conn.IsBackchannelReady() {
		return fmt.Errorf("speaker channel not connected")
	}

	if c.verbose {
		fmt.Printf("[Wyze] WriteAudio: codec=0x%02x, payload=%d bytes, rate=%d, ch=%d\n", codec, len(payload), sampleRate, channels)
	}

	return c.conn.AVSendAudioData(codec, payload, timestamp, sampleRate, channels)
}

func (c *Client) SetDeadline(t time.Time) error {
	if c.conn != nil {
		return c.conn.SetDeadline(t)
	}
	return nil
}

func (c *Client) Protocol() string {
	return "wyze/dtls"
}

func (c *Client) RemoteAddr() net.Addr {
	if c.conn != nil {
		return c.conn.RemoteAddr()
	}
	return nil
}

func (c *Client) Close() error {
	c.closeMu.Lock()
	if c.closed {
		c.closeMu.Unlock()
		return nil
	}
	c.closed = true
	c.closeMu.Unlock()

	if c.verbose {
		fmt.Printf("[Wyze] Closing connection\n")
	}

	c.StopIntercom()

	if c.conn != nil {
		c.conn.Close()
	}

	if c.verbose {
		fmt.Printf("[Wyze] Connection closed\n")
	}

	return nil
}

func (c *Client) connect() error {
	host := c.host
	port := 0

	if idx := strings.Index(host, ":"); idx > 0 {
		if p, err := strconv.Atoi(host[idx+1:]); err == nil {
			port = p
		}
		host = host[:idx]
	}

	conn, err := dtls.DialDTLS(host, port, c.uid, c.authKey, c.enr, c.verbose)
	if err != nil {
		return fmt.Errorf("wyze: connect failed: %w", err)
	}

	c.conn = conn
	if c.verbose {
		fmt.Printf("[Wyze] Connected to %s (IOTC + DTLS)\n", conn.RemoteAddr())
	}

	return nil
}

func (c *Client) doAVLogin() error {
	if c.verbose {
		fmt.Printf("[Wyze] Sending AV Login\n")
	}

	if err := c.conn.AVClientStart(5 * time.Second); err != nil {
		return fmt.Errorf("wyze: av login failed: %w", err)
	}

	if c.verbose {
		fmt.Printf("[Wyze] AV Login response received\n")
	}
	return nil
}

func (c *Client) doKAuth() error {
	// Step 1: K10000 -> K10001 (Challenge)
	data, err := c.conn.WriteAndWaitIOCtrl(c.buildK10000(), c.matchHL(KCmdChallenge), 5*time.Second)
	if err != nil {
		return fmt.Errorf("wyze: K10001 failed: %w", err)
	}

	hlData := c.extractHL(data)
	challenge, status, err := c.parseK10001(hlData)
	if err != nil {
		return fmt.Errorf("wyze: K10001 parse failed: %w", err)
	}

	if c.verbose {
		fmt.Printf("[Wyze] K10001 challenge received, status=%d\n", status)
	}

	// Step 2: K10002 -> K10003 (Auth)
	data, err = c.conn.WriteAndWaitIOCtrl(c.buildK10002(challenge, status), c.matchHL(KCmdAuthResult), 5*time.Second)
	if err != nil {
		return fmt.Errorf("wyze: K10002 failed: %w", err)
	}
	hlData = c.extractHL(data)

	// Parse K10003 response
	authResp, err := c.parseK10003(hlData)
	if err != nil {
		return fmt.Errorf("wyze: K10003 parse failed: %w", err)
	}

	if c.verbose && authResp != nil {
		if jsonBytes, err := json.MarshalIndent(authResp, "", "  "); err == nil {
			fmt.Printf("[Wyze] K10003 response:\n%s\n", jsonBytes)
		}
	}

	// Extract audio capability from cameraInfo
	if authResp != nil && authResp.CameraInfo != nil {
		if channelResult, ok := authResp.CameraInfo["channelRequestResult"].(map[string]any); ok {
			if audio, ok := channelResult["audio"].(string); ok {
				c.hasAudio = audio == "1"
			} else {
				c.hasAudio = true
			}
		} else {
			c.hasAudio = true
		}
	} else {
		c.hasAudio = true
	}

	if c.verbose {
		fmt.Printf("[Wyze] K10003 auth success\n")
	}

	c.hasIntercom = c.conn.HasTwoWayStreaming()

	if c.verbose {
		fmt.Printf("[Wyze] K-auth complete\n")
	}

	return nil
}

func (c *Client) buildK10000() []byte {
	json := []byte(`{"cameraInfo":{"audioEncoderList":[137,138,140]}}`) // 137=PCMU, 138=PCMA, 140=PCM
	b := make([]byte, 16+len(json))
	copy(b, "HL")                                           // magic
	b[2] = 5                                                // version
	binary.LittleEndian.PutUint16(b[4:], KCmdAuth)          // 10000
	binary.LittleEndian.PutUint16(b[6:], uint16(len(json))) // payload len
	copy(b[16:], json)
	return b
}

func (c *Client) buildK10002(challenge []byte, status byte) []byte {
	resp := generateChallengeResponse(challenge, c.enr, status)
	sessionID := make([]byte, 4)
	rand.Read(sessionID)
	b := make([]byte, 38)
	copy(b, "HL")                                           // magic
	b[2] = 5                                                // version
	binary.LittleEndian.PutUint16(b[4:], KCmdChallengeResp) // 10002
	b[6] = 22                                               // payload len
	copy(b[16:], resp[:16])                                 // challenge response
	copy(b[32:], sessionID)                                 // random session ID
	b[36] = 1                                               // video enabled/disabled
	b[37] = 1                                               // audio enabled/disabled
	return b
}

func (c *Client) buildK10010(mediaType byte, enabled bool) []byte {
	b := make([]byte, 18)
	copy(b, "HL")                                            // magic
	b[2] = 5                                                 // version
	binary.LittleEndian.PutUint16(b[4:], KCmdControlChannel) // 10010
	binary.LittleEndian.PutUint16(b[6:], 2)                  // payload len
	b[16] = mediaType                                        // 1=video, 2=audio, 3=return audio
	b[17] = 1                                                // 1=enable, 2=disable
	if !enabled {
		b[17] = 2
	}
	return b
}

func (c *Client) buildK10052(frameSize uint8, bitrate uint16) []byte {
	b := make([]byte, 22)
	copy(b, "HL")                                             // magic
	b[2] = 5                                                  // version
	binary.LittleEndian.PutUint16(b[4:], KCmdSetResolutionDB) // 10052
	binary.LittleEndian.PutUint16(b[6:], 6)                   // payload len
	binary.LittleEndian.PutUint16(b[16:], bitrate)            // bitrate (2 bytes)
	b[18] = frameSize + 1                                     // frame size (1 byte)
	// b[19] = fps, b[20:22] = zeros
	return b
}

func (c *Client) buildK10056(frameSize uint8, bitrate uint16) []byte {
	b := make([]byte, 21)
	copy(b, "HL")                                           // magic
	b[2] = 5                                                // version
	binary.LittleEndian.PutUint16(b[4:], KCmdSetResolution) // 10056
	binary.LittleEndian.PutUint16(b[6:], 5)                 // payload len
	b[16] = frameSize + 1                                   // frame size
	binary.LittleEndian.PutUint16(b[17:], bitrate)          // bitrate
	// b[19:21] = FPS (0 = auto)
	return b
}

func (c *Client) parseK10001(data []byte) (challenge []byte, status byte, err error) {
	if c.verbose {
		fmt.Printf("[Wyze] parseK10001: received %d bytes\n", len(data))
	}

	if len(data) < 33 {
		return nil, 0, fmt.Errorf("data too short: %d bytes", len(data))
	}

	if data[0] != 'H' || data[1] != 'L' {
		return nil, 0, fmt.Errorf("invalid HL magic: %x %x", data[0], data[1])
	}

	cmdID := binary.LittleEndian.Uint16(data[4:])
	if cmdID != KCmdChallenge {
		return nil, 0, fmt.Errorf("expected cmdID 10001, got %d", cmdID)
	}

	status = data[16]
	challenge = make([]byte, 16)
	copy(challenge, data[17:33])

	return challenge, status, nil
}

func (c *Client) parseK10003(data []byte) (*AuthResponse, error) {
	if c.verbose {
		fmt.Printf("[Wyze] parseK10003: received %d bytes\n", len(data))
	}

	if len(data) < 16 {
		return &AuthResponse{}, nil
	}

	if data[0] != 'H' || data[1] != 'L' {
		return &AuthResponse{}, nil
	}

	cmdID := binary.LittleEndian.Uint16(data[4:])
	textLen := binary.LittleEndian.Uint16(data[6:])

	if cmdID != KCmdAuthResult {
		return &AuthResponse{}, nil
	}

	if len(data) > 16 && textLen > 0 {
		jsonData := data[16:]
		for i := range jsonData {
			if jsonData[i] == '{' {
				var resp AuthResponse
				if err := json.Unmarshal(jsonData[i:], &resp); err == nil {
					if c.verbose {
						fmt.Printf("[Wyze] parseK10003: parsed JSON\n")
					}
					return &resp, nil
				}
				break
			}
		}
	}

	return &AuthResponse{}, nil
}

func (c *Client) useDoorbellResolution() bool {
	switch c.model {
	case "WYZEDB3", "WVOD1", "HL_WCO2", "WYZEC1":
		return true
	}
	return false
}

func (c *Client) hdFrameSize() uint8 {
	if c.isFloodlight() {
		return FrameSizeFloodlight
	}
	if c.is2K() {
		return FrameSize2K
	}
	return FrameSize1080P
}

func (c *Client) is2K() bool {
	switch c.model {
	case "HL_CAM3P", "HL_PANP", "HL_CAM4", "HL_DB2", "HL_CFL2":
		return true
	}
	return false
}

func (c *Client) isFloodlight() bool {
	return c.model == "HL_CFL2"
}

func (c *Client) matchHL(expectCmd uint16) func([]byte) bool {
	return func(data []byte) bool {
		hlData := c.extractHL(data)
		if hlData == nil {
			return false
		}
		cmd, _, ok := tutk.ParseHL(hlData)
		return ok && cmd == expectCmd
	}
}

func (c *Client) extractHL(data []byte) []byte {
	// Try offset 32 (magicIOCtrl, protoVersion)
	if hlData := tutk.FindHL(data, 32); hlData != nil {
		return hlData
	}
	// Try offset 36 (magicChannelMsg)
	if len(data) >= 36 && data[16] == 0x00 {
		return tutk.FindHL(data, 36)
	}
	return nil
}

const (
	statusDefault byte = 1
	statusENR16   byte = 3
	statusENR32   byte = 6
)

func generateChallengeResponse(challengeBytes []byte, enr string, status byte) []byte {
	var secretKey []byte

	switch status {
	case statusDefault:
		secretKey = []byte("FFFFFFFFFFFFFFFF")
	case statusENR16:
		if len(enr) >= 16 {
			secretKey = []byte(enr[:16])
		} else {
			secretKey = make([]byte, 16)
			copy(secretKey, enr)
		}
	case statusENR32:
		if len(enr) >= 16 {
			firstKey := []byte(enr[:16])
			challengeBytes = tutk.XXTEADecryptVar(challengeBytes, firstKey)
		}
		if len(enr) >= 32 {
			secretKey = []byte(enr[16:32])
		} else if len(enr) > 16 {
			secretKey = make([]byte, 16)
			copy(secretKey, []byte(enr[16:]))
		} else {
			secretKey = []byte("FFFFFFFFFFFFFFFF")
		}
	default:
		secretKey = []byte("FFFFFFFFFFFFFFFF")
	}

	return tutk.XXTEADecryptVar(challengeBytes, secretKey)
}
