/*
** Job Arranger for ZABBIX
** Copyright (C) 2025 Daiwa Institute of Research Ltd. All Rights Reserved.
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/
package common

import (
	"bytes"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"os"
	"strconv"
	"strings"
	"time"
)

type NetConnection struct {
	net.Conn
	receiveTimeout time.Duration
	sendTimeout    time.Duration
}

// if timeOutInSec is zero, ReceiveTimeout will be reset to no timeout
func (connection *NetConnection) SetReceiveTimeout(timeOutInSec int64) {
	connection.receiveTimeout = time.Duration(timeOutInSec) * time.Second
}

// if timeOutInSec is zero, SendTimeout will be reset to no timeout
func (connection *NetConnection) SetSendTimeout(timeOutInSec int64) {
	connection.sendTimeout = time.Duration(timeOutInSec) * time.Second
}

func (connection *NetConnection) Receive() (map[string]any, error) {
	var rawBuffer bytes.Buffer
	var receivedData map[string]any

	if connection.Conn == nil {
		return nil, errors.New("connection is nil")
	}

	if connection.receiveTimeout > 0 {
		connection.Conn.SetReadDeadline(time.Now().Add(connection.receiveTimeout))
	} else {
		connection.Conn.SetReadDeadline(time.Time{})
	}

	// Peek the first 4 bytes
	peekBuf := make([]byte, 4)
	_, err := io.ReadFull(connection.Conn, peekBuf)
	if err != nil {
		return nil, fmt.Errorf("failed to read prefix: %w", err)
	}

	var decoder *json.Decoder

	if string(peekBuf) == "ZBXD" {
		// Zabbix header detected
		headerRest := make([]byte, 9)
		if _, err := io.ReadFull(connection.Conn, headerRest); err != nil {
			return nil, fmt.Errorf("failed to read full Zabbix header: %w", err)
		}

		payloadLen := binary.LittleEndian.Uint64(headerRest[1:])
		if payloadLen == 0 {
			return nil, errors.New("payload length is zero")
		}

		payload := make([]byte, payloadLen)
		if _, err := io.ReadFull(connection.Conn, payload); err != nil {
			return nil, fmt.Errorf("failed to read payload: %w", err)
		}

		// Copy payload to raw buffer for logging
		rawBuffer.Write(payload)
		decoder = json.NewDecoder(&rawBuffer)
	} else {
		// Non-Zabbix — prepend peeked + conn using TeeReader
		reader := io.MultiReader(bytes.NewReader(peekBuf), connection.Conn)
		tee := io.TeeReader(reader, &rawBuffer)
		decoder = json.NewDecoder(tee)
	}

	decoder.UseNumber() // unmarshal as a Number instead of a float64 for interface{} values

	err = decoder.Decode(&receivedData)
	if err != nil {
		return nil, fmt.Errorf("json.NewDecoder() failed, %s, raw_data_hax: %s, raw_data_string: %s", err.Error(), hex.EncodeToString(rawBuffer.Bytes()), rawBuffer.String())
	}

	return receivedData, nil
}

func (connection *NetConnection) Send(v any) error {
	if connection.Conn == nil {
		return errors.New("connection is nil")
	}

	rawByte, err := json.Marshal(v)
	if err != nil {
		return fmt.Errorf("json.Marshal() failed, %s, raw_data: %s", err.Error(), string(rawByte))
	}

	// Append a newline delimiter
	rawByte = append(rawByte, '\n')

	if connection.sendTimeout > 0 {
		connection.Conn.SetWriteDeadline(time.Now().Add(connection.sendTimeout))
	} else {
		connection.Conn.SetWriteDeadline(time.Time{})
	}

	_, err = connection.Conn.Write(rawByte)
	if err != nil {
		return fmt.Errorf("connection.Conn.Write() failed, %s, raw_data: %s", err.Error(), string(rawByte))
	}

	return nil
}

func (connection *NetConnection) Close() error {
	if connection.Conn == nil {
		return errors.New("conn is nil")
	}

	return connection.Conn.Close()
}

func (connection *NetConnection) FcopyReceive(v any) error {
	const (
		zabbixHeader  = "ZBXD\x01"
		headerLength  = 13 // 5 (magic) + 8 (length)
		payloadOffset = 13 // after header
	)

	header := make([]byte, headerLength)

	// Read the fixed-length header first
	if _, err := io.ReadFull(connection.Conn, header); err != nil {
		return fmt.Errorf("failed to read header: %w", err)
	}

	if string(header[:5]) != "ZBXD\x01" {
		return fmt.Errorf("invalid Zabbix header: %x", header[:5])
	}

	// Payload length is stored in the next 8 bytes (little-endian)
	payloadLength := binary.LittleEndian.Uint64(header[5:13])

	// Read the actual JSON payload
	payload := make([]byte, payloadLength)
	if _, err := io.ReadFull(connection.Conn, payload); err != nil {
		return fmt.Errorf("failed to read payload: %w", err)
	}

	// For debugging
	fmt.Printf("Raw JSON payload: %s\n", string(payload))

	// Unmarshal into the provided interface
	if err := json.Unmarshal(payload, v); err != nil {
		return fmt.Errorf("JSON unmarshal failed: %w", err)
	}

	return nil
}

func SendSIG(pidFilepath string, sig os.Signal) error {
	byteData, err := os.ReadFile(pidFilepath)
	if err != nil {
		return fmt.Errorf("os.ReadFile failed, err: %s", err.Error())
	}

	helperPid, err := strconv.Atoi(strings.TrimSuffix(string(byteData), "\n"))
	if err != nil {
		return fmt.Errorf("strconv.Atoi failed, err: %s", err.Error())
	}

	if helperPid <= 0 {
		return fmt.Errorf("pid is less than or equqal zero")
	}

	p, err := os.FindProcess(helperPid)
	if err != nil {
		return fmt.Errorf("os.FindProcess failed, err: %s", err.Error())
	}

	err = p.Signal(sig)
	if err != nil {
		return fmt.Errorf("p.Signal failed, err: %s", err.Error())
	}

	return nil
}
