package dataformat

import (
	"bytes"
	"fmt"
	"io"
	"mime"
	"mime/multipart"
	"net/textproto"

	mapsutil "github.com/projectdiscovery/utils/maps"
)

type MultiPartForm struct {
	boundary      string
	filesMetadata map[string]FileMetadata
}

type FileMetadata struct {
	ContentType string
	Filename    string
}

var (
	_ DataFormat = &MultiPartForm{}
)

// NewMultiPartForm returns a new MultiPartForm encoder
func NewMultiPartForm() *MultiPartForm {
	return &MultiPartForm{
		filesMetadata: make(map[string]FileMetadata),
	}
}

// SetFileMetadata sets the file metadata for a given field name
func (m *MultiPartForm) SetFileMetadata(fieldName string, metadata FileMetadata) {
	if m.filesMetadata == nil {
		m.filesMetadata = make(map[string]FileMetadata)
	}

	m.filesMetadata[fieldName] = metadata
}

// GetFileMetadata gets the file metadata for a given field name
func (m *MultiPartForm) GetFileMetadata(fieldName string) (FileMetadata, bool) {
	if m.filesMetadata == nil {
		return FileMetadata{}, false
	}

	metadata, exists := m.filesMetadata[fieldName]

	return metadata, exists
}

// IsType returns true if the data is MultiPartForm encoded
func (m *MultiPartForm) IsType(data string) bool {
	// This method should be implemented to detect if the data is multipart form encoded
	return false
}

// Encode encodes the data into MultiPartForm format
func (m *MultiPartForm) Encode(data KV) (string, error) {
	var b bytes.Buffer
	w := multipart.NewWriter(&b)
	if err := w.SetBoundary(m.boundary); err != nil {
		return "", err
	}

	var Itererr error
	data.Iterate(func(key string, value any) bool {
		var fw io.Writer
		var err error

		if fileMetadata, ok := m.filesMetadata[key]; ok {
			if filesArray, isArray := value.([]any); isArray {
				for _, file := range filesArray {
					h := make(textproto.MIMEHeader)
					h.Set("Content-Disposition",
						fmt.Sprintf(`form-data; name=%q; filename=%q`,
							key, fileMetadata.Filename))
					h.Set("Content-Type", fileMetadata.ContentType)

					if fw, err = w.CreatePart(h); err != nil {
						Itererr = err
						return false
					}

					if _, err = fw.Write([]byte(file.(string))); err != nil {
						Itererr = err
						return false
					}
				}

				return true
			}
		}

		// Add field
		var values []string
		switch v := value.(type) {
		case nil:
			values = []string{""}
		case string:
			values = []string{v}
		case []string:
			values = v
		case []any:
			values = make([]string, len(v))
			for i, item := range v {
				if item == nil {
					values[i] = ""
				} else {
					values[i] = fmt.Sprint(item)
				}
			}
		default:
			values = []string{fmt.Sprintf("%v", v)}
		}

		for _, val := range values {
			if fw, err = w.CreateFormField(key); err != nil {
				Itererr = err
				return false
			}
			if _, err = fw.Write([]byte(val)); err != nil {
				Itererr = err
				return false
			}
		}
		return true
	})
	if Itererr != nil {
		return "", Itererr
	}

	_ = w.Close()
	return b.String(), nil
}

// ParseBoundary parses the boundary from the content type
func (m *MultiPartForm) ParseBoundary(contentType string) error {
	_, params, err := mime.ParseMediaType(contentType)
	if err != nil {
		return err
	}
	m.boundary = params["boundary"]
	if m.boundary == "" {
		return fmt.Errorf("no boundary found in the content type")
	}

	// NOTE(dwisiswant0): boundary cannot exceed 70 characters according to
	// RFC-2046.
	if len(m.boundary) > 70 {
		return fmt.Errorf("boundary exceeds maximum length of 70 characters")
	}

	return nil
}

// Decode decodes the data from MultiPartForm format
func (m *MultiPartForm) Decode(data string) (KV, error) {
	if m.boundary == "" {
		return KV{}, fmt.Errorf("boundary not set, call ParseBoundary first")
	}

	// Create a buffer from the string data
	b := bytes.NewBufferString(data)
	r := multipart.NewReader(b, m.boundary)

	form, err := r.ReadForm(32 << 20) // 32MB is the max memory used to parse the form
	if err != nil {
		return KV{}, err
	}
	defer func() {
		_ = form.RemoveAll()
	}()

	result := mapsutil.NewOrderedMap[string, any]()
	for key, values := range form.Value {
		if len(values) > 1 {
			result.Set(key, values)
		} else {
			result.Set(key, values[0])
		}
	}

	if m.filesMetadata == nil {
		m.filesMetadata = make(map[string]FileMetadata)
	}

	for key, files := range form.File {
		fileContents := []interface{}{}
		var fileMetadataList []FileMetadata

		for _, fileHeader := range files {
			file, err := fileHeader.Open()
			if err != nil {
				return KV{}, err
			}

			buffer := new(bytes.Buffer)
			if _, err := buffer.ReadFrom(file); err != nil {
				_ = file.Close()

				return KV{}, err
			}
			_ = file.Close()

			fileContents = append(fileContents, buffer.String())

			fileMetadataList = append(fileMetadataList, FileMetadata{
				ContentType: fileHeader.Header.Get("Content-Type"),
				Filename:    fileHeader.Filename,
			})
		}

		result.Set(key, fileContents)

		// NOTE(dwisiswant0): store the first file's metadata instead of the
		// last one
		if len(fileMetadataList) > 0 {
			m.filesMetadata[key] = fileMetadataList[0]
		}
	}
	return KVOrderedMap(&result), nil
}

// Name returns the name of the encoder
func (m *MultiPartForm) Name() string {
	return "multipart/form-data"
}
