package util

import (
	"bytes"
	"context"
	"crypto/tls"
	"fmt"
	"log"
	"mime"
	"mime/quotedprintable"
	"net"
	"net/mail"
	"net/smtp"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"syscall"
	"text/template"
	"time"
)

type EmailRecipient struct {
	Email     string
	FirstName string // optional
	LastName  string // optional
}

const rawMessageTemplate = `From: Email-Reminder <{{.Sender}}>
To: {{.Recipient}}
Subject: {{.Subject}}
X-Auto-Response-Suppress: All
Content-Language: en
User-Agent: email-reminder/{{.AppVersion}}
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Disposition: inline
Content-Transfer-Encoding: quoted-printable

{{.Body}}`

func ParseBool(value string) (bool, error) {
	switch strings.ToLower(value) {
	case "no":
		return false, nil
	case "yes":
		return true, nil
	default:
		return strconv.ParseBool(value)
	}
}

// SecureExecCommand executes a command only if the binary has secure permissions.
// It validates that the executable is owned by root and not writable by group or others,
// and enforces a 5-second timeout to prevent hanging on malicious executables.
// This mitigates command injection vulnerabilities by ensuring system binaries haven't been compromised.
func SecureExecCommand(path string, args ...string) ([]byte, error) {
	info, err := os.Stat(path)
	if err != nil {
		return nil, fmt.Errorf("cannot stat executable: %w", err)
	}
	stat, ok := info.Sys().(*syscall.Stat_t)
	if !ok {
		return nil, fmt.Errorf("cannot get file ownership info")
	}

	// Check ownership and permissions
	if stat.Uid != 0 {
		return nil, fmt.Errorf("executable not owned by root (owner: %d)", stat.Uid)
	}
	if permissions := info.Mode().Perm(); permissions&0022 != 0 {
		return nil, fmt.Errorf("executable is writable by non-owner (mode: %o)", permissions)
	}

	// Execute with timeout
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	cmd := exec.CommandContext(ctx, path, args...)
	return cmd.Output()
}

// ValidateEmail validates email addresses using RFC5322 via mail.ParseAddress(),
// while also accepting local delivery formats supported by local mail servers.
func ValidateEmail(email string) error {
	if email == "" {
		return fmt.Errorf("email address is empty")
	}

	// RFC 3696 Errata 1690 specifies a maximum email address length of 254 characters
	// https://www.rfc-editor.org/errata/eid1690
	if len(email) > 254 {
		return fmt.Errorf("email address exceeds maximum length of 254 characters")
	}

	// Normalize addresses accepted by Postfix for RFC5322 parsing
	normalized := email
	if !strings.Contains(email, "@") {
		// Bare username (e.g., "root") - add @localhost for parsing
		normalized = email + "@localhost"
	} else if strings.HasPrefix(email, "@") {
		// @hostname format (e.g., "@example.com") - add dummy local part for parsing
		normalized = "dummy.user" + email
	}

	addr, err := mail.ParseAddress(normalized) // RFC5322
	if err != nil {
		return fmt.Errorf("invalid email format: %w", err)
	}

	// Ensure no display name was included
	if addr.Address != normalized {
		return fmt.Errorf("email address contains unexpected formatting")
	}

	return nil
}

func IsLastDayOfTheMonth(now time.Time) bool {
	tomorrow := now.AddDate(0, 0, 1)
	return tomorrow.Month() != now.Month()
}

// Formatted email address suitable for the To header (e.g., "John Doe <john@example.com>"), or just the email if no name is present
func (e EmailRecipient) NameAndEmail() string {
	// Combine first and last names
	name := strings.TrimSpace(strings.Join([]string{e.FirstName, e.LastName}, " "))

	if name == "" && e.Email == "" {
		return ""
	}

	address := mail.Address{Name: name, Address: e.Email}
	return address.String()
}

func AssembleRawMessage(body string, subject string, recipient string, sender string, appVersion string) string {
	body = strings.ReplaceAll(body, "\n", "\r\n") // required by RFC822
	var qpBody bytes.Buffer
	w := quotedprintable.NewWriter(&qpBody)
	w.Write([]byte(body))
	w.Close()

	msgTemplate := template.Must(
		template.New("raw-message").Parse(strings.ReplaceAll(rawMessageTemplate, "\n", "\r\n")),
	)

	var msg strings.Builder
	msgTemplate.Execute(&msg, struct {
		Sender     string
		Recipient  string
		Subject    string
		AppVersion string
		Body       string
	}{
		Sender:     sender,
		Recipient:  recipient,
		Subject:    mime.QEncoding.Encode("UTF-8", subject),
		AppVersion: appVersion,
		Body:       qpBody.String(),
	})

	return msg.String()
}

// SendMailPlaintext sends email via plain SMTP on port 25 without any TLS/STARTTLS.
// This is useful for localhost connections where STARTTLS might fail due to certificate mismatches.
func SendMailPlaintext(host string, auth smtp.Auth, from string, to []string, msg []byte) error {
	conn, err := net.Dial("tcp", net.JoinHostPort(host, "25"))
	if err != nil {
		return fmt.Errorf("failed to establish connection: %w", err)
	}
	defer conn.Close()

	client, err := smtp.NewClient(conn, host)
	if err != nil {
		return fmt.Errorf("failed to create SMTP client: %w", err)
	}

	if auth != nil {
		if ok, _ := client.Extension("AUTH"); ok {
			if err := client.Auth(auth); err != nil {
				return fmt.Errorf("failed to authenticate with SMTP server: %w", err)
			}
		}
	}

	if err := client.Mail(from); err != nil {
		return fmt.Errorf("MAIL command failed for %s: %w", from, err)
	}
	for _, recipient := range to {
		if err := client.Rcpt(recipient); err != nil {
			return fmt.Errorf("RCPT command failed for %s: %w", recipient, err)
		}
	}

	data, err := client.Data()
	if err != nil {
		return fmt.Errorf("DATA command failed: %w", err)
	}
	if _, err := data.Write(msg); err != nil {
		return fmt.Errorf("failed to write message body: %w", err)
	}
	if err := data.Close(); err != nil {
		return fmt.Errorf("failed to close message body: %w", err)
	}

	if err := client.Quit(); err != nil {
		return fmt.Errorf("failed to terminate SMTP session: %w", err)
	}
	return nil
}

// Loosely based on https://gist.github.com/chrisgillis/10888032
func SendMailImplicitTLS(host string, auth smtp.Auth, from string, to []string, msg []byte) error {
	conn, err := tls.Dial("tcp", net.JoinHostPort(host, "465"), &tls.Config{
		ServerName: host,
		MinVersion: tls.VersionTLS12,
	})
	if err != nil {
		return fmt.Errorf("failed to establish TLS connection: %w", err)
	}
	defer conn.Close()

	client, err := smtp.NewClient(conn, host)
	if err != nil {
		return fmt.Errorf("failed to create SMTP client: %w", err)
	}

	if auth != nil {
		if ok, _ := client.Extension("AUTH"); ok {
			if err := client.Auth(auth); err != nil {
				return fmt.Errorf("failed to authenticate with SMTP server: %w", err)
			}
		}
	}

	if err := client.Mail(from); err != nil {
		return fmt.Errorf("MAIL command failed for %s: %w", from, err)
	}
	for _, recipient := range to {
		if err := client.Rcpt(recipient); err != nil {
			return fmt.Errorf("RCPT command failed for %s: %w", recipient, err)
		}
	}

	data, err := client.Data()
	if err != nil {
		return fmt.Errorf("DATA command failed: %w", err)
	}
	if _, err := data.Write(msg); err != nil {
		return fmt.Errorf("failed to write message body: %w", err)
	}
	if err := data.Close(); err != nil {
		return fmt.Errorf("failed to close message body: %w", err)
	}

	if err := client.Quit(); err != nil {
		return fmt.Errorf("failed to terminate SMTP session: %w", err)
	}
	return nil
}

// Ensure that the home directory is owned by the expected user
func ValidateHomeDirectory(homePath string, expectedUid uint32) error {
	info, err := os.Lstat(homePath) // Use Lstat to detect symlinks
	if err != nil {
		return fmt.Errorf("cannot access home directory '%s': %w", homePath, err)
	}

	if info.Mode()&os.ModeSymlink != 0 {
		return fmt.Errorf("home directory '%s' is a symlink", homePath)
	}
	if !info.IsDir() {
		return fmt.Errorf("home directory '%s' is not a directory", homePath)
	}

	if stat, ok := info.Sys().(*syscall.Stat_t); ok { // Only works on UNIX
		if stat.Uid != expectedUid {
			return fmt.Errorf("home directory '%s' is not owned by uid %d (actual owner: %d)", homePath, expectedUid, stat.Uid)
		}
	} else {
		log.Printf("cannot get ownership information for '%s'", homePath)
	}

	return nil
}

// ValidateOpenFile checks that an already-open file is owned by the expected user
// and is a regular file (not a device, socket, symlink, etc.).
func ValidateOpenFile(file *os.File, expectedUid uint32) error {
	info, err := file.Stat()
	if err != nil {
		return fmt.Errorf("cannot stat file '%s': %w", file.Name(), err)
	}

	stat, ok := info.Sys().(*syscall.Stat_t) // Only works on UNIX
	if !ok {
		return fmt.Errorf("cannot get ownership information for '%s'", file.Name())
	}

	if stat.Uid != expectedUid {
		return fmt.Errorf("file '%s' is not owned by uid %d (actual owner: %d)", file.Name(), expectedUid, stat.Uid)
	}

	if !info.Mode().IsRegular() {
		return fmt.Errorf("'%s' is not a regular file (mode: %s)", file.Name(), info.Mode().Type())
	}

	return nil
}

// Complete email body with friendly greeting and footer
func FullBody(body string, recipient EmailRecipient, version string) string {
	firstName := recipient.FirstName
	if firstName == "" {
		firstName = "there"
	}
	return fmt.Sprintf(`Hi %s,

%s

Have a good day!

--
Sent by Email-Reminder %s
https://launchpad.net/email-reminder`, firstName, body, version)
}
