// Copyright (c) 2026 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge 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 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.

package pmmime

import (
	"errors"
	"fmt"
	"maps"
	"strings"
	"unicode"

	"github.com/sirupsen/logrus"
)

// changeEncodingAndKeepLastParamDefinition is necessary to modify behaviour
// provided by the golang standard libraries.
func changeEncodingAndKeepLastParamDefinition(v string) (out string, err error) {
	log := logrus.WithField("pkg", "pm-mime")

	out = v // By default don't do anything with that.
	keepOrig := true

	i := strings.Index(v, ";")
	if i == -1 {
		i = len(v)
	}
	mediatype := strings.TrimSpace(strings.ToLower(v[0:i]))

	params := map[string]string{}
	var continuation map[string]map[string]string

	v = v[i:]
	for len(v) > 0 {
		v = strings.TrimLeftFunc(v, unicode.IsSpace)
		if len(v) == 0 {
			break
		}
		key, value, rest := consumeMediaParam(v)
		if key == "" {
			break
		}

		pmap := params
		if before, _, ok := strings.Cut(key, "*"); ok {
			baseName := before
			if continuation == nil {
				continuation = make(map[string]map[string]string)
			}
			var ok bool
			if pmap, ok = continuation[baseName]; !ok {
				continuation[baseName] = make(map[string]string)
				pmap = continuation[baseName]
			}
			if isFirstContinuation(key) {
				charset, _, err := get2231Charset(value)
				if err != nil {
					log.Errorln("Filter params:", err)
					v = rest
					continue
				}
				if charset != "utf-8" && charset != "us-ascii" {
					keepOrig = false
				}
			}
		}
		if _, exists := pmap[key]; exists {
			keepOrig = false
		}
		pmap[key] = value
		v = rest
	}

	if keepOrig {
		return
	}

	for paramKey, contMap := range continuation {
		value, err := mergeContinuations(paramKey, contMap)
		if err == nil {
			params[paramKey+"*"] = value
			continue
		}

		// Fallback.
		log.Errorln("Merge param", paramKey, ":", err)
		maps.Copy(params, contMap)
	}

	// Merge ;
	out = mediatype
	for k, v := range params {
		out += ";"
		out += k
		out += "="
		out += v
	}

	return
}

func isFirstContinuation(key string) bool {
	if idx := strings.Index(key, "*"); idx != -1 {
		return key[idx:] == "*" || key[idx:] == "*0*"
	}
	return false
}

// get2231Charset partially from mime/mediatype.go:211 function `decode2231Enc`.
func get2231Charset(v string) (charset, value string, err error) {
	sv := strings.SplitN(v, "'", 3)
	if len(sv) != 3 {
		err = errors.New("incorrect RFC2231 charset format")
		return
	}
	charset = strings.ToLower(sv[0])
	value = sv[2]
	return
}

func mergeContinuations(paramKey string, contMap map[string]string) (string, error) {
	var err error
	var charset, value string

	// Single value.
	if contValue, ok := contMap[paramKey+"*"]; ok {
		if charset, value, err = get2231Charset(contValue); err != nil {
			return "", err
		}
	} else {
		for n := 0; ; n++ {
			contKey := fmt.Sprintf("%s*%d", paramKey, n)
			contValue, isLast := contMap[contKey]
			if !isLast {
				var ok bool
				contValue, ok = contMap[contKey+"*"]
				if !ok {
					return "", errors.New("not valid RFC2231 continuation")
				}
			}
			if n == 0 {
				if charset, value, err = get2231Charset(contValue); err != nil || charset == "" {
					return "", err
				}
			} else {
				value += contValue
			}
			if isLast {
				break
			}
		}
	}

	return convertHexToUTF(charset, value)
}

// convertHexToUTF converts hex values string with charset to UTF8 in RFC2231 format.
func convertHexToUTF(charset, value string) (string, error) {
	raw, err := percentHexUnescape(value)
	if err != nil {
		return "", err
	}
	utf8, err := DecodeCharset(raw, "text/plain; charset="+charset)
	return "utf-8''" + percentHexEscape(utf8), err
}

// consumeMediaParam copy paste mime/mediatype.go:297.
func consumeMediaParam(v string) (param, value, rest string) {
	rest = strings.TrimLeftFunc(v, unicode.IsSpace)
	if !strings.HasPrefix(rest, ";") {
		return "", "", v
	}

	rest = rest[1:] // Consume semicolon.
	rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
	param, rest = consumeToken(rest)
	param = strings.ToLower(param)
	if param == "" {
		return "", "", v
	}

	rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
	if !strings.HasPrefix(rest, "=") {
		return "", "", v
	}
	rest = rest[1:] // Consume equals sign.
	rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
	value, rest2 := consumeValue(rest)
	if value == "" && rest2 == rest {
		return "", "", v
	}
	rest = rest2
	return param, value, rest
}

// consumeToken copy paste mime/mediatype.go:238.
// consumeToken consumes a token from the beginning of the provided string,
// per RFC 2045 section 5.1 (referenced from 2183), and returns
// the token consumed and the rest of the string.
// Returns ("", v) on failure to consume at least one character.
func consumeToken(v string) (token, rest string) {
	notPos := strings.IndexFunc(v, isNotTokenChar)
	if notPos == -1 {
		return v, ""
	}
	if notPos == 0 {
		return "", v
	}
	return v[0:notPos], v[notPos:]
}

// consumeValue copy paste mime/mediatype.go:253
// consumeValue consumes a "value" per RFC 2045, where a value is
// either a 'token' or a 'quoted-string'.  On success, consumeValue
// returns the value consumed (and de-quoted/escaped, if a
// quoted-string) and the rest of the string.
// On failure, returns ("", v).
func consumeValue(v string) (value, rest string) {
	if v == "" {
		return
	}
	if v[0] != '"' {
		return consumeToken(v)
	}

	// parse a quoted-string
	buffer := new(strings.Builder)
	for i := 1; i < len(v); i++ {
		r := v[i]
		if r == '"' {
			return buffer.String(), v[i+1:]
		}
		// When MSIE sends a full file path (in "intranet mode"), it does not
		// escape backslashes: "C:\dev\go\foo.txt", not "C:\\dev\\go\\foo.txt".
		//
		// No known MIME generators emit unnecessary backslash escapes
		// for simple token characters like numbers and letters.
		//
		// If we see an unnecessary backslash escape, assume it is from MSIE
		// and intended as a literal backslash. This makes Go servers deal better
		// with MSIE without affecting the way they handle conforming MIME
		// generators.
		if r == '\\' && i+1 < len(v) && !isTokenChar(rune(v[i+1])) {
			buffer.WriteByte(v[i+1])
			i++
			continue
		}
		if r == '\r' || r == '\n' {
			return "", v
		}
		buffer.WriteByte(v[i])
	}
	// Did not find end quote.
	return "", v
}

// isNotTokenChar copy paste from mime/mediatype.go:234.
func isNotTokenChar(r rune) bool {
	return !isTokenChar(r)
}

// isTokenChar copy paste from mime/grammar.go:19.
// isTokenChar reports whether rune is in 'token' as defined by RFC 1521 and RFC 2045.
func isTokenChar(r rune) bool {
	// token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
	//             or tspecials>
	return r > 0x20 && r < 0x7f && !isTSpecial(r)
}

// isTSpecial copy paste from mime/grammar.go:13
// isTSpecial reports whether rune is in 'tspecials' as defined by RFC
// 1521 and RFC 2045.
func isTSpecial(r rune) bool {
	return strings.ContainsRune(`()<>@,;:\"/[]?=`, r)
}

func percentHexEscape(raw []byte) (out string) {
	for _, v := range raw {
		out += fmt.Sprintf("%%%x", v)
	}
	return
}

// percentHexUnescape copy paste from mime/mediatype.go:325.
func percentHexUnescape(s string) ([]byte, error) {
	// Count %, check that they're well-formed.
	percents := 0
	for i := 0; i < len(s); {
		if s[i] != '%' {
			i++
			continue
		}
		percents++
		if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
			s = s[i:]
			if len(s) > 3 {
				s = s[0:3]
			}
			return []byte{}, fmt.Errorf("mime: bogus characters after %%: %q", s)
		}
		i += 3
	}
	if percents == 0 {
		return []byte(s), nil
	}

	t := make([]byte, len(s)-2*percents)
	j := 0
	for i := 0; i < len(s); {
		switch s[i] {
		case '%':
			t[j] = unhex(s[i+1])<<4 | unhex(s[i+2])
			j++
			i += 3
		default:
			t[j] = s[i]
			j++
			i++
		}
	}
	return t, nil
}

// ishex copy paste from mime/mediatype.go:364.
func ishex(c byte) bool {
	switch {
	case '0' <= c && c <= '9':
		return true
	case 'a' <= c && c <= 'f':
		return true
	case 'A' <= c && c <= 'F':
		return true
	}
	return false
}

// unhex copy paste from mime/mediatype.go:376.
func unhex(c byte) byte {
	switch {
	case '0' <= c && c <= '9':
		return c - '0'
	case 'a' <= c && c <= 'f':
		return c - 'a' + 10
	case 'A' <= c && c <= 'F':
		return c - 'A' + 10
	}
	return 0
}
