// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

package libddwaf

import (
	"fmt"

	"github.com/DataDog/go-libddwaf/v4/internal/bindings"
	"github.com/DataDog/go-libddwaf/v4/internal/ffi"
	"github.com/DataDog/go-libddwaf/v4/waferrors"
)

// decodeErrors transforms the wafObject received by the wafRulesetInfo after the call to wafDl.wafInit to a map where
// keys are the error message and the value is a array of all the rule ids which triggered this specific error
func decodeErrors(obj *bindings.WAFObject) (map[string][]string, error) {
	if !obj.IsMap() {
		return nil, fmt.Errorf("decodeErrors: %w: expected map, got %s", waferrors.ErrInvalidObjectType, obj.Type)
	}

	if obj.Value == 0 && obj.NbEntries == 0 {
		return nil, nil
	}

	if obj.Value == 0 && obj.NbEntries > 0 {
		return nil, waferrors.ErrNilObjectPtr
	}

	wafErrors := map[string][]string{}
	for i := uint64(0); i < obj.NbEntries; i++ {
		objElem := ffi.CastWithOffset[bindings.WAFObject](obj.Value, i)

		errorMessage := ffi.GostringSized(ffi.Cast[byte](objElem.ParameterName), objElem.ParameterNameLength)
		ruleIds, err := decodeStringArray(objElem)
		if err != nil {
			return nil, err
		}

		wafErrors[errorMessage] = ruleIds
	}

	return wafErrors, nil
}

func decodeDiagnostics(obj *bindings.WAFObject) (Diagnostics, error) {
	if !obj.IsMap() {
		return Diagnostics{}, fmt.Errorf("decodeDiagnostics: %w: expected map, got %s", waferrors.ErrInvalidObjectType, obj.Type)
	}
	if obj.Value == 0 && obj.NbEntries > 0 {
		return Diagnostics{}, waferrors.ErrNilObjectPtr
	}

	var (
		diags Diagnostics
		err   error
	)
	for i := uint64(0); i < obj.NbEntries; i++ {
		objElem := ffi.CastWithOffset[bindings.WAFObject](obj.Value, i)
		key := ffi.GostringSized(ffi.Cast[byte](objElem.ParameterName), objElem.ParameterNameLength)
		switch key {
		case "actions":
			diags.Actions, err = decodeFeature(objElem)
		case "custom_rules":
			diags.CustomRules, err = decodeFeature(objElem)
		case "exclusions":
			diags.Exclusions, err = decodeFeature(objElem)
		case "rules":
			diags.Rules, err = decodeFeature(objElem)
		case "rules_data":
			diags.RulesData, err = decodeFeature(objElem)
		case "exclusion_data":
			diags.ExclusionData, err = decodeFeature(objElem)
		case "rules_override":
			diags.RulesOverrides, err = decodeFeature(objElem)
		case "processors":
			diags.Processors, err = decodeFeature(objElem)
		case "processor_overrides":
			diags.ProcessorOverrides, err = decodeFeature(objElem)
		case "scanners":
			diags.Scanners, err = decodeFeature(objElem)
		case "ruleset_version":
			diags.Version = ffi.GostringSized(ffi.Cast[byte](objElem.Value), objElem.NbEntries)
		default:
			// ignore?
		}
		if err != nil {
			return Diagnostics{}, err
		}
	}

	return diags, nil
}

func decodeFeature(obj *bindings.WAFObject) (*Feature, error) {
	if !obj.IsMap() {
		return nil, fmt.Errorf("decodeFeature: %w: expected map, got %s", waferrors.ErrInvalidObjectType, obj.Type)
	}
	if obj.Value == 0 && obj.NbEntries > 0 {
		return nil, waferrors.ErrNilObjectPtr
	}
	var feature Feature
	var err error

	for i := uint64(0); i < obj.NbEntries; i++ {
		objElem := ffi.CastWithOffset[bindings.WAFObject](obj.Value, i)
		key := ffi.GostringSized(ffi.Cast[byte](objElem.ParameterName), objElem.ParameterNameLength)
		switch key {
		case "error":
			feature.Error = ffi.GostringSized(ffi.Cast[byte](objElem.Value), objElem.NbEntries)
		case "errors":
			feature.Errors, err = decodeErrors(objElem)
		case "failed":
			feature.Failed, err = decodeStringArray(objElem)
		case "loaded":
			feature.Loaded, err = decodeStringArray(objElem)
		case "skipped":
			feature.Skipped, err = decodeStringArray(objElem)
		case "warnings":
			feature.Warnings, err = decodeErrors(objElem)
		default:
			return nil, fmt.Errorf("%w: %s", waferrors.ErrUnsupportedValue, key)
		}

		if err != nil {
			return nil, err
		}
	}

	return &feature, nil
}

func decodeStringArray(obj *bindings.WAFObject) ([]string, error) {
	// We consider that nil is an empty array
	if obj.IsNil() {
		return nil, nil
	}

	if !obj.IsArray() {
		return nil, fmt.Errorf("decodeStringArray: %w: expected array, got %s", waferrors.ErrInvalidObjectType, obj.Type)
	}

	if obj.Value == 0 && obj.NbEntries > 0 {
		return nil, waferrors.ErrNilObjectPtr
	}

	if obj.NbEntries == 0 {
		return nil, nil
	}

	strArr := make([]string, 0, obj.NbEntries)
	for i := uint64(0); i < obj.NbEntries; i++ {
		objElem := ffi.CastWithOffset[bindings.WAFObject](obj.Value, i)
		if objElem.Type != bindings.WAFStringType {
			return nil, fmt.Errorf("decodeStringArray: %w: expected string, got %s", waferrors.ErrInvalidObjectType, objElem.Type)
		}

		strArr = append(strArr, ffi.GostringSized(ffi.Cast[byte](objElem.Value), objElem.NbEntries))
	}

	return strArr, nil
}

// Deprecated: This is merely wrapping [bindings.WAFObject.AnyValue], which should be used directly
// instead.
func DecodeObject(obj *WAFObject) (any, error) {
	return obj.AnyValue()
}
