//go:build !wasm && !lib_g3mail_disabled

/*
 * AxonASP Server
 * Copyright (C) 2026 G3pix Ltda. All rights reserved.
 *
 * Developed by Lucas Guimarães - G3pix Ltda
 * Contact: https://g3pix.com.br
 * Project URL: https://g3pix.com.br/axonasp
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 *
 * Attribution Notice:
 * If this software is used in other projects, the name "AxonASP Server"
 * must be cited in the documentation or "About" section.
 *
 * Contribution Policy:
 * Modifications to the core source code of AxonASP Server must be
 * made available under this same license terms.
 */
package axonvm

import (
	"fmt"
	"os"
	"strconv"
	"strings"

	"g3pix.com.br/axonasp/vbscript"
	"gopkg.in/gomail.v2"
)

type G3MailRelatedPart struct {
	filepath string
	cid      string
	bp       *G3Mail
}

type G3Mail struct {
	vm           *VM
	progID       string
	kind         int // 0 = message, 1 = body part, 2 = fields collection
	host         string
	port         int
	username     string
	password     string
	from         string
	fromName     string
	to           []string
	cc           []string
	bcc          []string
	subject      string
	body         string
	htmlBody     string
	isHTML       bool
	attachments  []string
	relatedParts []*G3MailRelatedPart
	filepath     string
	cid          string
	fields       map[string]string
	bpRef        *G3Mail
	charSet      string
	contentType  string
	replyTo      []string
}

// newG3MailObject instantiates the G3Mail custom functions library.
func (vm *VM) newG3MailObject() Value {
	return vm.newG3MailObjectWithProgID("g3mail")
}

// newG3MailObjectWithProgID instantiates the G3Mail custom functions library with a specific ProgID.
func (vm *VM) newG3MailObjectWithProgID(progID string) Value {
	obj := &G3Mail{
		vm:           vm,
		progID:       progID,
		kind:         0,
		to:           make([]string, 0),
		cc:           make([]string, 0),
		bcc:          make([]string, 0),
		attachments:  make([]string, 0),
		relatedParts: make([]*G3MailRelatedPart, 0),
		fields:       make(map[string]string),
		replyTo:      make([]string, 0),
	}
	id := vm.nextDynamicNativeID
	vm.nextDynamicNativeID++
	vm.g3mailItems[id] = obj
	return Value{Type: VTNativeObject, Num: id}
}

// DispatchPropertyGet acts as a getter.
func (m *G3Mail) DispatchPropertyGet(propertyName string) Value {
	lowerProp := strings.ToLower(propertyName)

	if m.kind == 1 { // Body part
		switch lowerProp {
		case "fields":
			fldObj := &G3Mail{
				vm:    m.vm,
				kind:  2,
				bpRef: m,
			}
			id := m.vm.nextDynamicNativeID
			m.vm.nextDynamicNativeID++
			m.vm.g3mailItems[id] = fldObj
			return Value{Type: VTNativeObject, Num: id}
		}
		return NewEmpty()
	}

	if m.kind == 2 { // Fields collection
		switch lowerProp {
		case "item":
			id := m.vm.nextDynamicNativeID
			m.vm.nextDynamicNativeID++
			m.vm.g3mailItems[id] = m
			return m.vm.newNativeObjectProxy(id, "Item", nil)
		}
		return NewEmpty()
	}

	switch lowerProp {
	case "htmlbody":
		if m.htmlBody != "" {
			return NewString(m.htmlBody)
		}
		if m.isHTML {
			return NewString(m.body)
		}
		return NewString("")
	case "host", "mailhost", "smtpserver", "remotehost":
		return NewString(m.host)
	case "port", "smtpserverport":
		return NewInteger(int64(m.port))
	case "username", "user", "authusername":
		return NewString(m.username)
	case "password", "pass", "authpassword":
		return NewString(m.password)
	case "from", "fromaddress":
		return NewString(m.from)
	case "fromname":
		return NewString(m.fromName)
	case "to":
		return NewString(strings.Join(m.to, ","))
	case "cc":
		return NewString(strings.Join(m.cc, ","))
	case "bcc":
		return NewString(strings.Join(m.bcc, ","))
	case "subject":
		return NewString(m.subject)
	case "body", "textbody", "message", "bodytext":
		return NewString(m.body)
	case "ishtml":
		return NewBool(m.isHTML)
	case "bodyformat", "mailformat":
		if m.isHTML {
			return NewInteger(0)
		}
		return NewInteger(1)
	case "charset":
		return NewString(m.charSet)
	case "contenttype":
		if m.contentType != "" {
			return NewString(m.contentType)
		}
		if m.isHTML {
			return NewString("text/html")
		}
		return NewString("text/plain")
	}
	return m.DispatchMethod(propertyName, nil)
}

// DispatchPropertySet acts as a setter.
func (m *G3Mail) DispatchPropertySet(propertyName string, args []Value) bool {
	if len(args) == 0 {
		return false
	}
	value := args[0]
	valueStr := m.vm.valueToString(value)
	lowerProp := strings.ToLower(propertyName)

	if m.kind == 1 || m.kind == 2 {
		return false
	}

	switch lowerProp {
	case "htmlbody":
		m.htmlBody = valueStr
		m.isHTML = true
		return true
	case "host", "mailhost", "smtpserver", "remotehost":
		m.host = strings.TrimSpace(valueStr)
		return true
	case "port", "smtpserverport":
		m.port = int(m.vm.asInt(value))
		return true
	case "username", "user", "authusername":
		m.username = strings.TrimSpace(valueStr)
		return true
	case "password", "pass", "authpassword":
		m.password = valueStr
		return true
	case "from", "fromaddress":
		m.from = strings.TrimSpace(valueStr)
		return true
	case "fromname":
		m.fromName = valueStr
		return true
	case "to":
		m.to = m.parseAddressList(valueStr)
		return true
	case "cc":
		m.cc = m.parseAddressList(valueStr)
		return true
	case "bcc":
		m.bcc = m.parseAddressList(valueStr)
		return true
	case "subject":
		m.subject = valueStr
		return true
	case "body", "message", "textbody", "bodytext":
		m.body = valueStr
		m.isHTML = false
		return true
	case "ishtml":
		m.isHTML = value.Type == VTBool && value.Num != 0
		return true
	case "bodyformat", "mailformat":
		m.isHTML = m.vm.asInt(value) == 0
		return true
	case "charset":
		m.charSet = valueStr
		return true
	case "contenttype":
		m.contentType = valueStr
		if strings.Contains(strings.ToLower(valueStr), "html") {
			m.isHTML = true
		} else {
			m.isHTML = false
		}
		return true
	}
	return false
}

// DispatchMethod provides O(1) string matching resolution.
func (m *G3Mail) DispatchMethod(methodName string, args []Value) Value {
	lowerMethod := strings.ToLower(methodName)

	if m.kind == 2 { // Fields collection
		switch lowerMethod {
		case "item":
			if len(args) == 2 {
				key := strings.ToLower(args[0].String())
				val := args[1].String()
				if m.bpRef != nil {
					m.bpRef.fields[key] = val
					if key == "urn:schemas:mailheader:content-id" {
						cleaned := val
						if strings.HasPrefix(cleaned, "<") && strings.HasSuffix(cleaned, ">") {
							cleaned = cleaned[1 : len(cleaned)-1]
						}
						m.bpRef.cid = cleaned
					}
				}
				return NewBool(true)
			} else if len(args) == 1 {
				key := strings.ToLower(args[0].String())
				if m.bpRef != nil {
					if val, ok := m.bpRef.fields[key]; ok {
						return NewString(val)
					}
				}
				return NewEmpty()
			}
		case "update":
			return NewBool(true)
		}
		return NewEmpty()
	}

	if m.kind == 1 { // Body part
		return NewEmpty()
	}

	switch lowerMethod {
	case "addaddress", "addrecipient", "addto":
		if len(args) == 0 {
			return NewBool(false)
		}
		var email, name string
		if len(args) == 1 {
			email = args[0].String()
		} else {
			arg0 := args[0].String()
			arg1 := args[1].String()
			if strings.EqualFold(lowerMethod, "addrecipient") || m.progID == "smtpsvg.mailer" {
				name = arg0
				email = arg1
			} else if m.progID == "persits.mailsender" {
				email = arg0
				name = arg1
			} else {
				if strings.Contains(arg0, "@") && !strings.Contains(arg1, "@") {
					email = arg0
					name = arg1
				} else if strings.Contains(arg1, "@") && !strings.Contains(arg0, "@") {
					email = arg1
					name = arg0
				} else {
					email = arg0
					name = arg1
				}
			}
		}
		addr := formatAddress(email, name)
		if addr != "" {
			m.to = append(m.to, addr)
		}
		return NewBool(true)

	case "addcc":
		if len(args) == 0 {
			return NewBool(false)
		}
		var email, name string
		if len(args) == 1 {
			email = args[0].String()
		} else {
			arg0 := args[0].String()
			arg1 := args[1].String()
			if m.progID == "smtpsvg.mailer" {
				name = arg0
				email = arg1
			} else if m.progID == "persits.mailsender" {
				email = arg0
				name = arg1
			} else {
				if strings.Contains(arg0, "@") && !strings.Contains(arg1, "@") {
					email = arg0
					name = arg1
				} else if strings.Contains(arg1, "@") && !strings.Contains(arg0, "@") {
					email = arg1
					name = arg0
				} else {
					email = arg0
					name = arg1
				}
			}
		}
		addr := formatAddress(email, name)
		if addr != "" {
			m.cc = append(m.cc, addr)
		}
		return NewBool(true)

	case "addbcc":
		if len(args) == 0 {
			return NewBool(false)
		}
		var email, name string
		if len(args) == 1 {
			email = args[0].String()
		} else {
			arg0 := args[0].String()
			arg1 := args[1].String()
			if m.progID == "smtpsvg.mailer" {
				name = arg0
				email = arg1
			} else if m.progID == "persits.mailsender" {
				email = arg0
				name = arg1
			} else {
				if strings.Contains(arg0, "@") && !strings.Contains(arg1, "@") {
					email = arg0
					name = arg1
				} else if strings.Contains(arg1, "@") && !strings.Contains(arg0, "@") {
					email = arg1
					name = arg0
				} else {
					email = arg0
					name = arg1
				}
			}
		}
		addr := formatAddress(email, name)
		if addr != "" {
			m.bcc = append(m.bcc, addr)
		}
		return NewBool(true)

	case "addreplyto":
		if len(args) > 0 {
			addr := strings.TrimSpace(args[0].String())
			if addr != "" {
				m.replyTo = append(m.replyTo, addr)
			}
		}
		return NewBool(true)

	case "addattachment":
		if len(args) > 0 {
			filepath := args[0].String()
			if _, err := os.Stat(filepath); err != nil {
				m.vm.raise(vbscript.FileNotFound, fmt.Sprintf("File not found: %s", filepath))
				return NewEmpty()
			}
			m.attachments = append(m.attachments, filepath)
			return NewBool(true)
		}
		return NewBool(false)

	case "addrelatedbodypart":
		if len(args) >= 2 {
			filepath := args[0].String()
			cid := args[1].String()

			if _, err := os.Stat(filepath); err != nil {
				m.vm.raise(vbscript.FileNotFound, fmt.Sprintf("File not found: %s", filepath))
				return NewEmpty()
			}

			bp := &G3Mail{
				vm:       m.vm,
				kind:     1,
				filepath: filepath,
				cid:      cid,
				fields:   make(map[string]string),
			}

			m.relatedParts = append(m.relatedParts, &G3MailRelatedPart{
				filepath: filepath,
				cid:      cid,
				bp:       bp,
			})

			id := m.vm.nextDynamicNativeID
			m.vm.nextDynamicNativeID++
			m.vm.g3mailItems[id] = bp
			return Value{Type: VTNativeObject, Num: id}
		}
		return NewEmpty()

	case "send", "sendmail":
		if len(args) >= 3 {
			// CDONTS.NewMail style args: To, Subject, Body
			m.to = m.parseAddressList(args[0].String())
			m.subject = args[1].String()
			m.body = args[2].String()
			m.isHTML = false
		}
		return m.sendInternal()

	case "clear":
		m.to = []string{}
		m.cc = []string{}
		m.bcc = []string{}
		m.subject = ""
		m.body = ""
		m.htmlBody = ""
		m.isHTML = false
		m.attachments = []string{}
		m.relatedParts = []*G3MailRelatedPart{}
		m.replyTo = []string{}
		return NewBool(true)

	case "clearaddresses", "clearrecipients":
		m.to = []string{}
		return NewBool(true)

	case "clearcc", "clearccs":
		m.cc = []string{}
		return NewBool(true)

	case "clearbcc", "clearbccs":
		m.bcc = []string{}
		return NewBool(true)

	case "clearattachments":
		m.attachments = []string{}
		return NewBool(true)
	}

	return NewEmpty()
}

func (m *G3Mail) sendInternal() Value {
	host := strings.TrimSpace(m.host)
	port := m.port
	username := strings.TrimSpace(m.username)
	password := m.password
	from := strings.TrimSpace(m.from)

	if host == "" || port <= 0 || username == "" || password == "" || from == "" {
		envHost, envPort, envUser, envPass, envFrom, envErr := m.getSMTPConfigFromEnv()
		if envErr != nil {
			return NewString(fmt.Sprintf("Error: %v", envErr))
		}
		if host == "" {
			host = envHost
		}
		if port <= 0 {
			port = envPort
		}
		if username == "" {
			username = envUser
		}
		if password == "" {
			password = envPass
		}
		if from == "" {
			from = envFrom
		}
	}

	allRecipients := append([]string{}, m.to...)
	allRecipients = append(allRecipients, m.cc...)
	allRecipients = append(allRecipients, m.bcc...)
	if len(allRecipients) == 0 {
		return NewString("Error: Missing recipients")
	}

	to := strings.Join(allRecipients, ",")

	msg := gomail.NewMessage()
	msg.SetHeader("From", from)
	msg.SetHeader("To", to)
	msg.SetHeader("Subject", m.subject)

	if len(m.replyTo) > 0 {
		msg.SetHeader("Reply-To", strings.Join(m.replyTo, ","))
	}

	textContentType := "text/plain"
	htmlContentType := "text/html"
	if m.charSet != "" {
		textContentType = "text/plain; charset=" + m.charSet
		htmlContentType = "text/html; charset=" + m.charSet
	}

	if m.htmlBody != "" {
		if m.body != "" {
			msg.SetBody(textContentType, m.body)
			msg.AddAlternative(htmlContentType, m.htmlBody)
		} else {
			msg.SetBody(htmlContentType, m.htmlBody)
		}
	} else if m.isHTML {
		msg.SetBody(htmlContentType, m.body)
	} else {
		msg.SetBody(textContentType, m.body)
	}

	for _, filepath := range m.attachments {
		msg.Attach(filepath)
	}

	for _, part := range m.relatedParts {
		cid := part.bp.cid
		if cid == "" {
			cid = part.cid
		}
		msg.Embed(part.filepath, gomail.SetHeader(map[string][]string{
			"Content-ID": {"<" + cid + ">"},
		}))
	}

	d := gomail.NewDialer(host, port, username, password)

	if err := d.DialAndSend(msg); err != nil {
		return NewString(fmt.Sprintf("Error sending email: %v", err))
	}
	return NewBool(true)
}

func (m *G3Mail) parseAddressList(value string) []string {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return []string{}
	}
	parts := strings.FieldsFunc(trimmed, func(r rune) bool {
		return r == ';' || r == ','
	})
	out := make([]string, 0, len(parts))
	for _, part := range parts {
		candidate := strings.TrimSpace(part)
		if candidate != "" {
			out = append(out, candidate)
		}
	}
	return out
}

func (m *G3Mail) getSMTPConfigFromEnv() (host string, port int, username, password, from string, err error) {
	host = strings.TrimSpace(os.Getenv("SMTP_HOST"))
	portStr := strings.TrimSpace(os.Getenv("SMTP_PORT"))
	username = strings.TrimSpace(os.Getenv("SMTP_USER"))
	password = strings.TrimSpace(os.Getenv("SMTP_PASS"))
	from = strings.TrimSpace(os.Getenv("SMTP_FROM"))

	if host == "" || portStr == "" || username == "" || password == "" || from == "" {
		return "", 0, "", "", "", fmt.Errorf("SMTP environment variables not set")
	}

	parsedPort, parseErr := strconv.Atoi(portStr)
	if parseErr != nil {
		return "", 0, "", "", "", fmt.Errorf("SMTP_PORT is not a valid number")
	}

	return host, parsedPort, username, password, from, nil
}

func formatAddress(email, name string) string {
	name = strings.TrimSpace(name)
	email = strings.TrimSpace(email)
	if name == "" {
		return email
	}
	if strings.ContainsAny(name, `",@<>`) || strings.Contains(name, " ") {
		return fmt.Sprintf("%q <%s>", name, email)
	}
	return fmt.Sprintf("%s <%s>", name, email)
}
