package utils

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"jobarranger2/src/libs/golibs/common"
	"path/filepath"
	"strconv"
	"strings"
	"unicode"
)

func RawJsonToMapStringAny(from json.RawMessage) (map[string]any, error) {
	var err error
	var to map[string]any

	dec := json.NewDecoder(bytes.NewReader(from))
	dec.UseNumber()
	if err := dec.Decode(&to); err != nil {
		err = fmt.Errorf("Decode() failed, err: %w", err)
		return to, err
	}
	return to, err
}

// Encrypt with AES
func EncryptPassword(data string) (string, error) {
	// AES key (32 bytes)
	secret := "Oc7odHSwiXU1w3OY8kD2yQzp9GLkYd1Cl821Coqp4uM="
	sum := sha256.Sum256([]byte(secret))
	key := sum[:]

	// Create AES block cipher
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}

	// Create GCM mode
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}

	// 12-byte IV (nonce) — required for GCM
	iv := make([]byte, gcm.NonceSize())
	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
		return "", err
	}

	// Encrypt
	// Seal returns: ciphertext || tag
	ciphertextWithTag := gcm.Seal(nil, iv, []byte(data), nil)

	// Split tag and ciphertext to match PHP layout
	tagSize := gcm.Overhead() // 16 bytes
	tag := ciphertextWithTag[len(ciphertextWithTag)-tagSize:]
	ciphertext := ciphertextWithTag[:len(ciphertextWithTag)-tagSize]

	// Final payload: IV || TAG || CIPHERTEXT
	payload := append(append(iv, tag...), ciphertext...)

	return "2:" + base64.StdEncoding.EncodeToString(payload), nil
}

// Decrypt AES encrypted string
func DecryptPassword(encryptedText string) (string, error) {
	const secret = "Oc7odHSwiXU1w3OY8kD2yQzp9GLkYd1Cl821Coqp4uM="

	// 1. Validate prefix
	if !strings.HasPrefix(encryptedText, "2:") {
		return "", errors.New("unsupported encryption format")
	}

	// 2. Derive AES key (same as PHP: hash('sha256', secret, true))
	key := sha256.Sum256([]byte(secret))

	// 3. Base64 decode (strip "2:")
	raw, err := base64.StdEncoding.DecodeString(encryptedText[2:])
	if err != nil {
		return "", err
	}

	// 4. Extract fields
	if len(raw) < 12+16 {
		return "", errors.New("invalid encrypted payload")
	}

	iv := raw[:12]
	tag := raw[12:28]
	ciphertext := raw[28:]

	// 5. Create AES-GCM
	block, err := aes.NewCipher(key[:])
	if err != nil {
		return "", err
	}

	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}

	// 6. Ciphertext||tag (tag appended)
	combined := append(ciphertext, tag...)

	// 7. Decrypt
	plaintext, err := gcm.Open(nil, iv, combined, nil)
	if err != nil {
		return "", err
	}

	return string(plaintext), nil
}

// Decrypt AES encrypted agentless login password
func DecryptAESLessString(pw string) (string, error) {
	// Decrypt combined encrypted string
	combinedString, err := DecryptPassword(pw)
	if err != nil {
		return "", fmt.Errorf("failed to decrypt login password : %w", err)
	}

	// Find start of AES payload
	pos := strings.LastIndex(combinedString, "2:")
	if pos == -1 {
		return "", fmt.Errorf("unsupported password format : %v", combinedString)
	}

	// Decrypt login password
	decryptedPW, err := DecryptPassword(combinedString[pos:])
	if err != nil {
		return "", fmt.Errorf("failed to decrypt login password : %w", err)
	}

	// Check pw length
	pos = strings.IndexByte(decryptedPW, '|')
	if pos == -1 {
		return "", fmt.Errorf("unsupported password format : %v", decryptedPW)
	}

	pwLen := decryptedPW[:pos]
	decryptedPW = decryptedPW[pos+1:]

	expectedLen, err := strconv.Atoi(pwLen)
	if err != nil {
		return "", fmt.Errorf("unsupported password format : %v", decryptedPW)
	}

	if expectedLen != len(decryptedPW) {
		return "", fmt.Errorf("unsupported password format : %v", decryptedPW)
	}

	return decryptedPW, nil
}

// Decrypt Custom encrypted agentless icon login password
func DecryptLegacyLessString(dPasswd string) string {
	dFlag := "1"
	key := "199907"
	var passwd string

	// check if dPasswd should be decoded
	if len(dPasswd) > 0 && dPasswd[0] == dFlag[0] {
		j, k := 0, 0
		dec := make([]byte, 0, len(dPasswd))

		// step 1: decode hex -> char and xor with key
		for kk := 1; kk < len(dPasswd); kk++ {
			if kk%2 == 0 { // even index
				hexStr := "0x" + string([]byte{dPasswd[kk-1], dPasswd[kk]})
				x16toX10, err := strconv.ParseInt(hexStr, 0, 64)
				if err != nil {
					return dPasswd // fallback
				}
				decodedChar := byte(x16toX10) ^ key[j]
				dec = append(dec, decodedChar)
				j++
				k++
				if j == len(key) {
					j = 0
				}
			}
		}

		// step 2: split at '|'
		decStr := string(dec)
		searchIndex := -1
		for i, c := range decStr {
			if c == '|' {
				searchIndex = i
				break
			}
		}

		if searchIndex == -1 {
			// no '|' found → fallback
			passwd = dPasswd
		} else {
			dNum := decStr[:searchIndex]

			// validate dNum is numeric
			isNumeric := true
			for _, c := range dNum {
				if !unicode.IsDigit(c) {
					isNumeric = false
					break
				}
			}

			if isNumeric {
				numVal, _ := strconv.Atoi(dNum)
				if numVal == len(decStr)-searchIndex-1 {
					// extract substring after '|'
					passwd = decStr[searchIndex+1:]
				} else {
					passwd = dPasswd
				}
			} else {
				passwd = dPasswd
			}
		}
	} else {
		passwd = dPasswd
	}

	return passwd
}

// Decrypt Custom encrypted job icon run user password
func DecryptLegacyJobString(d_passwd string) string {
	key := "199907"
	var dec []byte
	j := 0
	k := 0

	if len(d_passwd) == 0 {
		return d_passwd
	}

	// Only decode if first character matches d_flag ('1')
	d_flag := "1"
	if d_passwd[0] == d_flag[0] {
		dec = make([]byte, len(d_passwd)/2+1) // max needed
		d_x16 := make([]byte, 3)              // for hex pair
		for kk := 1; kk < len(d_passwd); kk++ {
			if kk%2 != 0 {
				d_x16[0] = d_passwd[kk]
			} else {
				d_x16[1] = d_passwd[kk]
				d_x16[2] = 0
				// Convert hex string to int
				hexStr := string(d_x16[:2])
				x16toX10, err := strconv.ParseUint(hexStr, 16, 8)
				if err != nil {
					return d_passwd
				}
				// XOR with key
				dec[k] = byte(x16toX10) ^ key[j]
				j++
				k++
				if j == len(key) {
					j = 0
				}
			}
		}
		dec[k] = 0 // null-terminate, optional in Go

		// return as string
		return string(dec[:k])
	}

	// if d_flag not set, return original
	return d_passwd
}

func GetFirstFileSource(files []common.FileTransfer) (string, error) {
	if len(files) == 0 {
		return "", fmt.Errorf("empty transfer files")
	}
	if files[0].Source == "" {
		return "", fmt.Errorf("source file is empty")
	}
	return files[0].Source, nil

}

// ParseTransactionFileName parses a transaction filename into its component parts.
// Expected format:
//
//	<InnerJobnetID>_<JobnetID>_<InnerJobID>_<Timestamp>.<Extension>
//
// Example:
//
//	1500000000000000275_MYJOBNET_223_20250912145641_612638856.json
func ParseTransactionFileName(filePath string) (*common.EventFileNameParts, error) {
	filename := filepath.Base(filePath)

	ext := filepath.Ext(filename)
	if ext == "" {
		return nil, fmt.Errorf("missing file extension in filename: %s", filename)
	}

	base := strings.TrimSuffix(filename, ext)
	parts := strings.Split(base, "_")
	if len(parts) < 5 {
		return nil, fmt.Errorf("invalid filename format: %s", filename)
	}

	// Parse InnerJobnetID
	innerJobnetID, err := strconv.ParseUint(parts[0], 10, 64)
	if err != nil {
		return nil, fmt.Errorf("invalid InnerJobnetID [%s]: %w", parts[0], err)
	}

	// Parse InnerJobID
	innerJobID, err := strconv.ParseUint(parts[len(parts)-3], 10, 64)
	if err != nil {
		return nil, fmt.Errorf("invalid InnerJobID [%s]: %w", parts[len(parts)-2], err)
	}

	// Parse JobnetID
	jobnetID := strings.Join(parts[1:len(parts)-3], "")

	// Parse Timestamp
	timestamp := strings.Join(parts[len(parts)-2:], "_")

	return &common.EventFileNameParts{
		InnerJobnetID: innerJobnetID,
		JobnetID:      jobnetID,
		InnerJobID:    innerJobID,
		Timestamp:     timestamp,
		Extension:     strings.TrimPrefix(ext, "."),
		FilePath:      filePath,
	}, nil
}
