package tplink

// Credit to:
// sausheong - https://github.com/sausheong/hs1xxplug
// jaedle - https://github.com/jaedle/golang-tplink-hs100/blob/master/internal/connector/connector.go

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"log"
	"net"
	"time"

	"encoding/binary"
	"encoding/json"
)

// This is the key by which all bytes sent/received from tp-link hardware are
// obfuscated.
const hashKey byte = 0xAB

// SysInfo A type used to return information from tplink devices
type SysInfo struct {
	System struct {
		GetSysinfo struct {
			SwVer      string `json:"sw_ver"`
			HwVer      string `json:"hw_ver"`
			Type       string `json:"type"`
			Model      string `json:"model"`
			Mac        string `json:"mac"`
			DevName    string `json:"dev_name"`
			Alias      string `json:"alias"`
			RelayState int    `json:"relay_state"`
			OnTime     int    `json:"on_time"`
			ActiveMode string `json:"active_mode"`
			Feature    string `json:"feature"`
			Updating   int    `json:"updating"`
			IconHash   string `json:"icon_hash"`
			Rssi       int    `json:"rssi"`
			LedOff     int    `json:"led_off"`
			LongitudeI int    `json:"longitude_i"`
			LatitudeI  int    `json:"latitude_i"`
			HwID       string `json:"hwId"`
			FwID       string `json:"fwId"`
			DeviceID   string `json:"deviceId"`
			OemID      string `json:"oemId"`
			Children   []struct {
				ID         string `json:"id"`
				State      int    `json:"state"`
				Alias      string `json:"alias"`
				OnTime     int    `json:"on_time"`
				NextAction struct {
					Type int `json:"type"`
				} `json:"next_action"`
			} `json:"children"`
			ChildNum   int `json:"child_num"`
			NtcState   int `json:"ntc_state"`
			NextAction struct {
				Type int `json:"type"`
			} `json:"next_action"`
			ErrCode   int     `json:"err_code"`
			MicType   string  `json:"mic_type"`
			Latitude  float64 `json:"latitude"`
			Longitude float64 `json:"longitude"`
		} `json:"get_sysinfo"`
	} `json:"system"`
}

// Tplink Device host identification
type Tplink struct {
	Host     string
	SwitchID int
}

type getSysInfo struct {
	System struct {
		SysInfo struct {
		} `json:"get_sysinfo"`
	} `json:"system"`
}

type changeState struct {
	System struct {
		SetRelayState struct {
			State int `json:"state"`
		} `json:"set_relay_state"`
	} `json:"system"`
}

type changeStateMultiSwitch struct {
	Context struct {
		ChildIds []string `json:"child_ids"`
	} `json:"context"`
	System struct {
		SetRelayState struct {
			State int `json:"state"`
		} `json:"set_relay_state"`
	} `json:"system"`
}

func encrypt(plaintext string) []byte {
	n := len(plaintext)
	buf := new(bytes.Buffer)
	if err := binary.Write(buf, binary.BigEndian, uint32(n)); err != nil {
		log.Printf("[WARNING] tplink.go: Unable to write to buffer %v\n", err)
	}
	ciphertext := buf.Bytes()

	key := hashKey
	payload := make([]byte, n)
	for i := 0; i < n; i++ {
		payload[i] = plaintext[i] ^ key
		key = payload[i]
	}

	for i := 0; i < len(payload); i++ {
		ciphertext = append(ciphertext, payload[i])
	}

	return ciphertext
}

func decrypt(ciphertext []byte) string {
	n := len(ciphertext)
	key := hashKey
	var nextKey byte
	for i := 0; i < n; i++ {
		nextKey = ciphertext[i]
		ciphertext[i] ^= key
		key = nextKey
	}
	return string(ciphertext)
}

func send(host string, dataSend []byte) ([]byte, error) {
	var header = make([]byte, 4)

	// establish connection (two second timeout)
	conn, err := net.DialTimeout("tcp", host+":9999", time.Second*2) //nolint:gomnd
	if err != nil {
		return []byte{}, err
	}
	defer func() {
		if err := conn.Close(); err != nil { //nolint:govet
			log.Fatalf("[ERROR] tplink.go: Unable to close connection: %v\n", err)
		}
	}()

	// submit data to device
	writer := bufio.NewWriter(conn)
	_, err = writer.Write(dataSend)
	if err != nil {
		return []byte{}, err
	}
	if err := writer.Flush(); err != nil { //nolint:govet
		return []byte{}, err
	}

	// read response header to determine response size
	headerReader := io.LimitReader(conn, int64(4)) //nolint:gomnd
	_, err = headerReader.Read(header)
	if err != nil {
		return []byte{}, err
	}

	// read response
	respSize := int64(binary.BigEndian.Uint32(header))
	respReader := io.LimitReader(conn, respSize)
	var response = make([]byte, respSize)
	_, err = respReader.Read(response)
	if err != nil {
		return []byte{}, err
	}

	return response, nil
}

func getDevID(s *Tplink) (string, error) {
	info, err := s.SystemInfo()
	if err != nil {
		return "", err
	}
	return info.System.GetSysinfo.DeviceID, nil
}

// SystemInfo Returns information from targeted device
func (s *Tplink) SystemInfo() (SysInfo, error) {
	var (
		payload  getSysInfo
		jsonResp SysInfo
	)

	j, _ := json.Marshal(payload)

	data := encrypt(string(j))
	resp, err := send(s.Host, data)
	if err != nil {
		return jsonResp, err
	}

	if err := json.Unmarshal([]byte(decrypt(resp)), &jsonResp); err != nil {
		return jsonResp, err
	}
	return jsonResp, nil
}

// ChangeState changes the power state of a single port device
// True = on
// False = off
func (s *Tplink) ChangeState(state bool) error {
	var payload changeState

	if state {
		payload.System.SetRelayState.State = 1
	} else {
		payload.System.SetRelayState.State = 0
	}

	j, _ := json.Marshal(payload)
	data := encrypt(string(j))
	if _, err := send(s.Host, data); err != nil {
		return err
	}
	return nil
}

// ChangeStateMultiSwitch changes the power state of a device on with multiple outlets/switches
// True = on
// False = off
func (s *Tplink) ChangeStateMultiSwitch(state bool) error {
	var payload changeStateMultiSwitch

	devID, err := getDevID(s)
	if err != nil {
		return err
	}

	payload.Context.ChildIds = append(payload.Context.ChildIds, devID+fmt.Sprintf("%02d", s.SwitchID))
	if state {
		payload.System.SetRelayState.State = 1
	} else {
		payload.System.SetRelayState.State = 0
	}

	j, _ := json.Marshal(payload)
	data := encrypt(string(j))
	if _, err := send(s.Host, data); err != nil {
		return err
	}
	return nil
}