/*
** 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 utils

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"strconv"

	"jobarranger2/src/libs/golibs/common"
)

// Convert marshals the input data into JSON and unmarshals it into the out parameter.
//
// This is useful for converting between compatible types (e.g., map to struct or struct to struct),
// performing deep copies, or normalizing data.
//
// Parameters:
//   - input: the source data (can be a struct, map, etc.)
//   - out: pointer to the destination data structure (struct or map) where result will be stored
//
// Returns:
//   - error if marshal or unmarshal fails.
//
// Example usage:
//
//	var user User
//	err := Convert(dataMap, &user)
func Convert(input any, out any) error {
	bytes, err := json.Marshal(input)
	if err != nil {
		return fmt.Errorf("failed to marshal input: %w", err)
	}

	if err := json.Unmarshal(bytes, out); err != nil {
		return fmt.Errorf("failed to unmarshal into output: %w", err)
	}
	return nil
}

// Convert marshals the input data into JSON String
//
// Parameters:
//   - input: the source data (can be a struct, map, etc.)
//
// Returns:
//   - json String
//   - error if marshal or unmarshal fails.
//
// Example usage:
//
//	var user User
//	err := Convert(any)
func ConvertToJsonString(input any) (string, error) {
	out, err := json.Marshal(input)
	if err != nil {
		return "", fmt.Errorf("failed to marshal input: %w", err)
	}
	return string(out), err

}

// Convert marshals the input data into JSON String
//
// Parameters:
//   - input: the source data (can be a struct, map, etc.)
//
// Returns:
//   - json String
//   - string without json dereferences if marshal fails.
//
// Example usage:
//
//	var user User
//	userStr := ToReadableString(user)
func ToReadableString(v any) string {
	b, err := json.Marshal(v)
	if err != nil {
		return fmt.Sprintf("%v", v) // fallback
	}
	return string(b)
}

// Explanation
// MapToStruct takes a map[string]string/map[string]any as input and attempts to populate the provided struct pointer (out)
// by first marshaling the map into JSON and then unmarshaling it into the target struct.
//
// This is useful when working with loosely typed data (e.g., from dynamic sources like JSON APIs or interfaces),
// and you want to convert that into a strongly typed Go struct.
//
// Parameters:
//   - input: a map containing data with string keys and arbitrary values (map[string]string)
//   - out: a pointer to a struct where the data should be loaded
//
// Returns:
//   - error if marshalling or unmarshalling fails, otherwise nil.
//
// Example usage:
//
//	var user User
//	err := MapToStruct(dataMap, &user)
func MapToStruct(input any, out any) error {

	switch input.(type) {
	case map[string]any, map[string]string:
		// Valid map types, proceed
	default:
		return fmt.Errorf("input must be of type map[string]any or map[string]string")
	}

	jsonBytes, err := json.Marshal(input)
	if err != nil {
		return fmt.Errorf("failed to marshal map: %w", err)
	}

	if err := json.Unmarshal(jsonBytes, out); err != nil {
		return fmt.Errorf("failed to unmarshal into struct: %w", err)
	}

	return nil
}

// MapToJsonByte takes a map[string]string and converts it to a JSON-formatted byte slice.
//
// This is helpful when you need the map represented as raw JSON for storage, transmission,
// or further processing.
//
// Parameters:
//   - input: a map of string keys and values
//
// Returns:
//   - []byte: JSON-encoded representation of the map
//   - error: if JSON marshaling fails
//
// Example usage:
//
//	jsonBytes, err := MapToJsonByte(dataMap)
func MapToJsonByte(input map[string]string) ([]byte, error) {
	var jsonBytes []byte

	jsonBytes, err := json.Marshal(input)
	if err != nil {
		return nil, err
	}

	return jsonBytes, nil
}

// JsonByteTostruct takes a JSON-formatted byte slice and unmarshals it into the provided struct pointer.
//
// Useful for deserializing JSON data received over a network or read from a file into a Go struct.
//
// Parameters:
//   - input: byte slice containing JSON data
//   - out: a pointer to the struct to populate with the unmarshalled data
//
// Returns:
//   - error: if unmarshalling fails
//
// Example usage:
//
//	var user User
//	err := JsonByteTostruct(jsonBytes, &user)
func JsonByteTostruct(input []byte, out any) error {
	if err := json.Unmarshal(input, out); err != nil {
		return fmt.Errorf("failed to unmarshal into struct: %w", err)
	}

	return nil
}

func MapStringStringToStruct(inputMap map[string]string, out any) error {
	outVal := reflect.ValueOf(out)
	if outVal.Kind() != reflect.Pointer || outVal.Elem().Kind() != reflect.Struct {
		return errors.New("output must be a pointer to struct")
	}

	outStruct := outVal.Elem()
	outType := outStruct.Type()

	for i := 0; i < outStruct.NumField(); i++ {
		field := outStruct.Field(i)
		structField := outType.Field(i)

		jsonKey := structField.Tag.Get("json")
		if jsonKey == "" {
			jsonKey = structField.Name
		}

		val, ok := inputMap[jsonKey]
		if !ok || val == "" {
			continue
		}

		fieldType := field.Type()

		// Handle json.RawMessage
		if fieldType == reflect.TypeOf(json.RawMessage{}) {
			field.Set(reflect.ValueOf(json.RawMessage(val)))
			continue
		}

		if fieldType == reflect.TypeOf(map[string]interface{}{}) {
			var m map[string]interface{}
			if err := json.Unmarshal([]byte(val), &m); err != nil {
				return fmt.Errorf("failed to unmarshal field %s into map[string]interface{}: %w", jsonKey, err)
			}
			field.Set(reflect.ValueOf(m))
			continue
		}

		switch field.Kind() {
		case reflect.String:
			field.SetString(val)

		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			intVal, err := strconv.ParseInt(val, 10, 64)
			if err != nil {
				return fmt.Errorf("failed to convert field %s to int: %w", jsonKey, err)
			}
			field.SetInt(intVal)

		case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
			uintVal, err := strconv.ParseUint(val, 10, 64)
			if err != nil {
				return fmt.Errorf("failed to convert field %s to uint: %w", jsonKey, err)
			}
			field.SetUint(uintVal)

		case reflect.Float32, reflect.Float64:
			floatVal, err := strconv.ParseFloat(val, 64)
			if err != nil {
				return fmt.Errorf("failed to convert field %s to float: %w", jsonKey, err)
			}
			field.SetFloat(floatVal)
		case reflect.Bool:
			boolVal, err := strconv.ParseBool(val)
			if err != nil {
				return fmt.Errorf("failed to convert field %s to bool: %w", jsonKey, err)
			}
			field.SetBool(boolVal)
		case reflect.Interface:
			var ifaceVal interface{}
			err := json.Unmarshal([]byte(val), &ifaceVal)
			if err != nil {
				return fmt.Errorf("failed to unmarshal field %s into interface{}: %w", jsonKey, err)
			}
			field.Set(reflect.ValueOf(ifaceVal))
		}
	}

	return nil
}

func UnmarshalFlowData(data []byte, eventName common.EventName, processData *common.FlowProcessData) error {
	var err error
	tmpEvent := struct {
		InnerJobnetId uint64            `json:"inner_jobnet_id"`
		JobnetId      string            `json:"jobnet_id"`
		InnerJobId    uint64            `json:"inner_job_id"`
		IconType      common.IconType   `json:"icon_type"`
		FlowType      common.FlowType   `json:"flow_type"`
		Status        common.StatusType `json:"status"`
		ExitCode      int               `json:"exit_code"`
		Data          json.RawMessage   `json:"data"`
	}{}

	err = json.Unmarshal(data, &tmpEvent)
	if err != nil {
		return fmt.Errorf("Unmarshal(flowdata) failed, err: [%w]", err)
	}

	processData.InnerJobnetId = tmpEvent.InnerJobnetId
	processData.InnerJobId = tmpEvent.InnerJobId
	processData.JobnetId = tmpEvent.JobnetId
	processData.IconType = tmpEvent.IconType
	processData.FlowType = tmpEvent.FlowType
	processData.Status = tmpEvent.Status
	processData.ExitCode = tmpEvent.ExitCode
	processData.Data = tmpEvent.Data

	switch eventName {
	case common.EventIconResultEnd, common.EventIconResultRunErr,
		common.EventIconRetry, common.EventEndCountPlus,
		common.EventMIconRun, common.EventIconHostUnlock:
		var flowData common.IconExecutionProcessData

		err = json.Unmarshal([]byte(tmpEvent.Data), &flowData)
		if err != nil {
			return fmt.Errorf("Unmarshal(flowdata) failed, err: [%w]", err)
		}
		processData.Data = flowData
	}
	return err
}

// func (eventData *EventData) UnmarshalEventData(data []byte) error {
func UnmarshalEventData(data []byte, eventData *common.EventData) error {
	// Temporary holder to extract raw `data` field
	tmpEvent := struct {
		Event       common.Event `json:"event"`
		NextProcess struct {
			Name common.ProcessType `json:"name"`
			Data json.RawMessage    `json:"data"`
		} `json:"next_process"`
		TCPMessage *struct {
			SendRetry      *int            `json:"sendretry,omitempty"`
			Kind           string          `json:"kind"`
			Version        float64         `json:"version"`
			ServerID       string          `json:"serverid"`
			Hostname       string          `json:"hostname"`
			JazVersion     *int            `json:"jaz_version,omitempty"`
			EncryptVersion float64         `json:"encrypt_version"`
			Data           json.RawMessage `json:"data"`
		} `json:"tcp_message,omitempty"`
		DBSyncResult         common.DBSyncResult         `json:"dbsync_result"`
		SQLConditions        []common.SQLCondition       `json:"sql_conditions"`
		ParameterizedQueries []common.ParameterizedQuery `json:"parameterized_queries"`
		Transfer             common.Transfer             `json:"transfer"`
	}{}
	var err error

	dec := json.NewDecoder(bytes.NewReader(data))
	dec.UseNumber() // <-- force numbers to json.Number

	if err := dec.Decode(&tmpEvent); err != nil {
		return err
	}

	eventData.Event = tmpEvent.Event
	eventData.DBSyncResult = &tmpEvent.DBSyncResult
	eventData.SQLConditions = tmpEvent.SQLConditions
	eventData.ParameterizedQueries = tmpEvent.ParameterizedQueries
	eventData.Transfer = tmpEvent.Transfer
	eventData.NextProcess.Name = tmpEvent.NextProcess.Name
	if tmpEvent.TCPMessage != nil {
		eventData.TCPMessage = &common.TCPMessage{
			SendRetry:      tmpEvent.TCPMessage.SendRetry,
			Kind:           tmpEvent.TCPMessage.Kind,
			Version:        tmpEvent.TCPMessage.Version,
			ServerID:       tmpEvent.TCPMessage.ServerID,
			Hostname:       tmpEvent.TCPMessage.Hostname,
			JazVersion:     tmpEvent.TCPMessage.JazVersion,
			EncryptVersion: tmpEvent.TCPMessage.EncryptVersion,
		}
	}

	switch tmpEvent.Event.Name {
	case common.EventJobnetLoad, common.EventJobnetTimeout, common.EventDelayedStart,
		common.EventScheduleDelete, common.EventJobnetHold,
		common.EventIconRerunStatusSync, common.EventEndIconEnd,
		common.EventSubJobnetEnd, common.EventJobnetIconRun,
		common.EventJobnetStop, common.EventJobnetIconStop, common.EventIconRunErrSkip,
		common.EventJobnetRunError, common.EventDBJobnetRunQueryFailure, common.EventIconTimeoutWarning,
		common.EventStandAloneJob, common.EventScheduleUpdate, common.EventJobnetUnhold,
		common.EventFlowJobnetIconStatusChange:
		var processData common.JobnetRunData
		err = json.Unmarshal(tmpEvent.NextProcess.Data, &processData)
		eventData.NextProcess.Data = processData

	case common.EventJobnetRun,
		common.EventIconResultEnd, common.EventIconResultRunErr, common.EventIconResultEndErr,
		common.EventIconRerun, common.EventFailoverIconRerun, common.EventIconUnhold, common.EventEndCountPlus, common.EventIconTimeoutStop,
		common.EventIconStop, common.EventIconHoldSkip,
		common.EventIconRetry, common.EventSubJobnetStatusChange,
		common.EventFlowCheckAgent, common.EventFlowCheckLocal, common.EventFlowIconSkip, common.EventIconRerunJobnetStatusChange,
		common.EventMIconRun, common.EventIconHostUnlock:
		var processData common.FlowProcessData
		err = UnmarshalFlowData(tmpEvent.NextProcess.Data, tmpEvent.Event.Name, &processData)
		eventData.NextProcess.Data = processData

	case common.EventIconReady, common.EventIconResultUpdate, common.EventIconTimeout, common.EventIconSkip,
		common.EventIconAbort, common.EventOneWayIconRun, common.EventTwoWayIconRun,
		common.EventExecCheckLocal, common.EventExecCheckAgent,
		common.EventIconHold, common.EventFlowRunErr:
		var processData common.IconExecutionProcessData
		err = json.Unmarshal(tmpEvent.NextProcess.Data, &processData)
		eventData.NextProcess.Data = processData

	case common.EventIconExecRun, common.EventIconExecEnd:
		var processData common.IconRunData
		err = json.Unmarshal(tmpEvent.NextProcess.Data, &processData)
		eventData.NextProcess.Data = processData

	case common.EventAgentJobRun:
		var processData common.IconExecutionProcessData
		err = json.Unmarshal(tmpEvent.NextProcess.Data, &processData)
		eventData.NextProcess.Data = processData

		if err == nil {
			var tcpData common.JobRunRequestData
			err = json.Unmarshal(tmpEvent.TCPMessage.Data, &tcpData)
			eventData.TCPMessage.Data = tcpData
		}

	case common.EventAgentJobRan, common.EventAgentJobResult, common.EventAgentJobCheck:
		var processData common.IconExecutionProcessData
		err = json.Unmarshal(tmpEvent.NextProcess.Data, &processData)
		eventData.NextProcess.Data = processData

		if err == nil {
			var tcpData common.JobResultData
			err = json.Unmarshal(tmpEvent.TCPMessage.Data, &tcpData)
			eventData.TCPMessage.Data = tcpData
		}
	case common.EventAgentJobRunResp, common.EventAgentJobResultResp:
		var processData common.IconExecutionProcessData
		err = json.Unmarshal(tmpEvent.NextProcess.Data, &processData)
		eventData.NextProcess.Data = processData

		if err == nil {
			var tcpData common.ResponseData
			err = json.Unmarshal(tmpEvent.TCPMessage.Data, &tcpData)
			eventData.TCPMessage.Data = tcpData
		}
	case common.EventZabbixIconRun:
		var processData common.IconLinkData
		err = json.Unmarshal(tmpEvent.NextProcess.Data, &processData)
		eventData.NextProcess.Data = processData
	case common.EventZabbixHostIp:
		var processData common.HostData
		err = json.Unmarshal(tmpEvent.NextProcess.Data, &processData)
		eventData.NextProcess.Data = processData
	case common.EventInsertLogTable:
	case common.EventInsertSendMsg, common.EventInsertSendMsgTable:
		var processData common.ZbxSendInfo
		err = json.Unmarshal(tmpEvent.NextProcess.Data, &processData)
		eventData.NextProcess.Data = processData
	case common.EventJobnetEnd, common.EventJobnetEnding, common.EventJobnetStatusChange,
		common.EventScheduleDeleteDBSync, common.EventScheduleHoldDBSync, common.EventScheduleReleaseDBSync, common.EventJobnetDelayError,
		common.EventScheduleUpdateDBSync:
	default:
		return fmt.Errorf("invalid event")
	}

	return err
}
