// # Copyright (C) 2025 Francois Marier
//
// Email-Reminder 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.
//
// Email-Reminder 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 Email-Reminder; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
// 02110-1301, USA.
package events

import (
	"encoding/xml"
	"fmt"
	"log"
	"strconv"
	"strings"
	"time"

	"launchpad.net/email-reminder/internal/util"
)

const (
	hoursPerDay    = 24 * time.Hour
	maxNameLength  = 1024
	maxEmailLength = 254
)

type EventType string

const (
	EventTypeAnniversary EventType = "anniversary"
	EventTypeBirthday    EventType = "birthday"
	EventTypeYearly      EventType = "yearly"
	EventTypeMonthly     EventType = "monthly"
	EventTypeWeekly      EventType = "weekly"
)

func (et *EventType) UnmarshalText(text []byte) error {
	v := strings.TrimSpace(strings.ToLower(string(text)))
	switch v {
	case string(EventTypeAnniversary):
		*et = EventTypeAnniversary
	case string(EventTypeBirthday):
		*et = EventTypeBirthday
	case string(EventTypeYearly):
		*et = EventTypeYearly
	case string(EventTypeMonthly):
		*et = EventTypeMonthly
	case string(EventTypeWeekly):
		*et = EventTypeWeekly
	default:
		*et = EventType(v)
	}
	return nil
}

// Valid values: 0-6 for weekly events, 1-31 otherwise
type EventDay int

func (ed *EventDay) UnmarshalText(text []byte) error {
	value := strings.TrimSpace(strings.ToLower(string(text)))

	// Numeric values
	if day, err := strconv.Atoi(value); err == nil {
		*ed = EventDay(day)
		return nil
	}

	// Handle textual day names (like Perl's Date::Manip ParseDate)
	dayNames := map[string]int{
		"sunday":    0,
		"monday":    1,
		"tuesday":   2,
		"wednesday": 3,
		"thursday":  4,
		"friday":    5,
		"saturday":  6,
		"sun":       0,
		"mon":       1,
		"tue":       2,
		"wed":       3,
		"thu":       4,
		"fri":       5,
		"sat":       6,
	}

	if day, exists := dayNames[value]; exists {
		*ed = EventDay(day)
		return nil
	}

	*ed = EventDay(-1)
	return fmt.Errorf("unrecognized day name: %s", value)
}

type Event struct {
	XMLName      xml.Name    `xml:"event"`
	Type         EventType   `xml:"type,attr"`
	Name         string      `xml:"name"`
	Email        string      `xml:"email,omitempty"`
	PartnerName  string      `xml:"partner_name,omitempty"`
	PartnerEmail string      `xml:"partner_email,omitempty"`
	Reminders    []Reminder  `xml:"reminders>reminder,omitempty"`
	Recipients   []Recipient `xml:"recipients>recipient,omitempty"`
	Day          EventDay    `xml:"day,omitempty"`
	Month        time.Month  `xml:"month,omitempty"`
	Year         int         `xml:"year,omitempty"`
}

func (event Event) Validate() error {
	if strings.TrimSpace(event.Name) == "" {
		return fmt.Errorf("empty event name")
	}
	if len(event.Name) > maxNameLength {
		return fmt.Errorf("event name exceeds maximum length of %d characters", maxNameLength)
	}

	if len(event.Email) > maxEmailLength {
		return fmt.Errorf("email exceeds maximum length of %d characters", maxEmailLength)
	}
	if len(event.PartnerEmail) > maxEmailLength {
		return fmt.Errorf("partner email exceeds maximum length of %d characters", maxEmailLength)
	}

	if event.Year < 0 {
		return fmt.Errorf("%d is not a valid year", event.Year)
	}

	switch event.Type {
	case EventTypeAnniversary:
		if strings.TrimSpace(event.PartnerName) == "" {
			return fmt.Errorf("empty partner name")
		}
		if len(event.PartnerName) > maxNameLength {
			return fmt.Errorf("partner name exceeds maximum length of %d characters", maxNameLength)
		}
		fallthrough
	case EventTypeBirthday, EventTypeYearly:
		if event.Month < 1 || event.Month > 12 {
			return fmt.Errorf("%d is not a valid month", event.Month)
		}
		fallthrough
	case EventTypeMonthly:
		if event.Day < 1 || event.Day > 31 {
			return fmt.Errorf("%d is not a valid day", event.Day)
		}
	case EventTypeWeekly:
		if event.Day < 0 || event.Day > 7 {
			return fmt.Errorf("%d is not a valid day of the week", event.Day)
		}
	default:
		return fmt.Errorf("unknown event type: %s", event.Type)
	}
	return nil
}

func (event Event) IsOccurring(now time.Time, reminder Reminder) (bool, string, error) {
	target := now
	when := ""
	switch reminder.Type {
	case ReminderTypeSameDay:
		when = "today"
	case ReminderTypeDaysBefore:
		if reminder.Days <= 0 {
			return false, "", fmt.Errorf("invalid reminder days: %d", reminder.Days)
		}
		target = target.Add(hoursPerDay * time.Duration(reminder.Days))
		if reminder.Days == 1 {
			when = fmt.Sprintf("tomorrow (%s)", target.Format("Mon Jan 2"))
		} else {
			when = fmt.Sprintf("in %d days (%s)", reminder.Days, target.Format("Mon Jan 2"))
		}
	default:
		return false, "", fmt.Errorf("unknown reminder type: %s", reminder.Type)
	}

	switch event.Type {
	case EventTypeAnniversary, EventTypeBirthday, EventTypeYearly:
		return event.IsSameDay(target), when, nil
	case EventTypeMonthly:
		if int(event.Day) == target.Day() {
			return true, when, nil
		} else if int(event.Day) > 28 && util.IsLastDayOfTheMonth(target) {
			// If it's not possible for a monthly event to occur this month because there
			// aren't enough days in the month, then we assume the user wants this event
			// to occur on the last day of the month.
			return true, when, nil
		}
		return false, when, nil
	case EventTypeWeekly:
		return target.Weekday() == event.Day.DayOfTheWeek(), when, nil
	default:
		return false, when, fmt.Errorf("unknown event type: %s", event.Type)
	}
}

func (event Event) GetOccurrence(now time.Time) (int, bool) {
	if event.Year == 0 {
		return 0, false
	}

	occurrence := now.Year() - event.Year
	if now.Month() < event.Month {
		// Haven't quite reached the anniversary month yet
		occurrence -= 1
	} else if now.Month() == event.Month && now.Day() < int(event.Day) {
		// Handle Feb 29th events correctly
		if !event.IsSameDay(now) {
			// Haven't quite reached the anniversary day yet
			occurrence -= 1
		}
	}

	return occurrence, occurrence >= 0
}

func (event Event) GetRecipients(fallback util.EmailRecipient, maxRecipients int) []util.EmailRecipient {
	to := []util.EmailRecipient{}

	// Look for event-specific recipients
	for _, recipient := range event.Recipients {
		emailRecipient := recipient.ToEmailRecipient()
		if emailRecipient.Email == "" {
			log.Printf("Ignoring recipient %s: missing email address", recipient)
			continue
		}

		if len(to) >= maxRecipients {
			log.Printf("Warning: event '%s' has more than %d recipients, ignoring excess recipients", event.Name, maxRecipients)
			break
		}

		to = append(to, emailRecipient)
	}

	if len(to) == 0 && fallback.Email != "" {
		// Fall back to default destination email address if configured
		to = []util.EmailRecipient{fallback}
	}
	return to
}

func (event Event) AnniversaryContact() string {
	email := strings.TrimSpace(event.Email)
	partnerEmail := strings.TrimSpace(event.PartnerEmail)
	if email != "" && partnerEmail != "" {
		return fmt.Sprintf("You can reach them at %s and %s respectively.", email, partnerEmail)
	} else if email != "" {
		return fmt.Sprintf("You can reach %s at %s.", strings.TrimSpace(event.Name), email)
	} else if partnerEmail != "" {
		return fmt.Sprintf("You can reach %s at %s.", strings.TrimSpace(event.PartnerName), partnerEmail)
	}
	return "" // No contact details for either person
}

func (event Event) ReminderMessage(now time.Time, occurringWhen string) (string, string, error) {
	msg := ""
	subject := strings.TrimSpace(event.Name)
	name := strings.TrimSpace(event.Name)
	switch event.Type {
	case EventTypeAnniversary:
		partnerName := strings.TrimSpace(event.PartnerName)
		subject = fmt.Sprintf("Anniversary of %s and %s", name, partnerName)
		anniversaryString := "anniversary"
		specialName := ""
		if occurrence, valid := event.GetOccurrence(now.UTC()); valid {
			anniversaryString = fmt.Sprintf("%s anniversary", formatOrdinal(occurrence))
			if s := anniversaryName(occurrence); s != "" {
				specialName = fmt.Sprintf(" (%s)", s)
			}
			subject = fmt.Sprintf("%s of %s and %s", anniversaryString, name, partnerName)
		}
		msg = fmt.Sprintf(`I just want to remind you that the %s%s of %s and %s is %s.`, anniversaryString, specialName, name, partnerName, occurringWhen)
		if contactString := event.AnniversaryContact(); contactString != "" {
			msg += fmt.Sprintf("\n\n%s", contactString)
		}
	case EventTypeBirthday:
		subject = fmt.Sprintf("%s's birthday", name)
		age := "getting one year older"
		if occurrence, valid := event.GetOccurrence(now.UTC()); valid {
			age = fmt.Sprintf("turning %d", occurrence)
			if occurringWhen == "today" {
				subject = fmt.Sprintf("%s is now %d", name, occurrence)
			}
		}

		email := strings.TrimSpace(event.Email)
		fullName := name
		if email != "" {
			fullName = fmt.Sprintf("%s (%s)", name, email)
		}
		msg = fmt.Sprintf("I just want to remind you that %s is %s %s.", fullName, age, occurringWhen)
	case EventTypeMonthly, EventTypeWeekly:
		msg = fmt.Sprintf(`I just want to remind you of the following event %s:

%s`, occurringWhen, name)
	case EventTypeYearly:
		// Use supplied 'now' for deterministic testing instead of current system time
		if occurrence, valid := event.GetOccurrence(now.UTC()); valid {
			name = fmt.Sprintf("%s %s", formatOrdinal(occurrence+1), name)
		}
		msg = fmt.Sprintf(`I just want to remind you of the following event %s:

%s`, occurringWhen, name)
	default:
		return "", "", fmt.Errorf("unknown event type %s", event.Type)
	}
	return msg, subject, nil
}

func (event Event) String() string {
	name := strings.TrimSpace(event.Name)
	switch event.Type {
	case EventTypeBirthday:
		if event.Year > 0 {
			return fmt.Sprintf("%s's birthday (%d-%02d-%02d)", name, event.Year, event.Month, int(event.Day))
		}
		return fmt.Sprintf("%s's birthday (%02d-%02d)", name, event.Month, int(event.Day))
	case EventTypeAnniversary:
		return fmt.Sprintf("%s's anniversary (%d-%02d-%02d)", name, event.Year, event.Month, int(event.Day))
	case EventTypeMonthly:
		return fmt.Sprintf("%s (monthly on day %d)", name, int(event.Day))
	case EventTypeWeekly:
		return fmt.Sprintf("%s (weekly on %s)", name, event.Day.DayOfTheWeek())
	case EventTypeYearly:
		return fmt.Sprintf("%s (yearly on %02d-%02d)", name, event.Month, int(event.Day))
	default:
		return fmt.Sprintf("%s (%s)", name, event.Type)
	}
}

func (e Event) IsSameDay(now time.Time) bool {
	if now.Month() == time.February && now.Day() == 28 && int(e.Day) == 29 {
		// Feb 29th birthdays are typically celebrated on Feb 28th in non-leap years
		return util.IsLastDayOfTheMonth(now)
	}
	return now.Month() == e.Month && now.Day() == int(e.Day)
}

func (e EventDay) DayOfTheWeek() time.Weekday {
	day := int(e)
	if day < 1 || day > 6 {
		// Default to Sunday for out of range dates (both 0 and 7 are Sunday in cron)
		return time.Sunday
	}
	return time.Weekday(day)
}

func anniversaryName(occurrence int) string {
	// https://en.wikipedia.org/wiki/Wedding_anniversary#Traditional_anniversary_gifts
	names := map[int]string{
		1:  "Paper",
		2:  "Cotton",
		3:  "Leather",
		4:  "Linen",
		5:  "Wood",
		6:  "Iron",
		7:  "Copper",
		8:  "Bronze",
		9:  "Pottery",
		10: "Tin",
		11: "Steel",
		12: "Silk",
		13: "Lace",
		14: "Ivory",
		15: "Crystal",
		20: "Porcelain",
		25: "Silver",
		30: "Pearl",
		35: "Coral",
		40: "Ruby",
		45: "Sapphire",
		50: "Gold",
		55: "Emerald",
		60: "Diamond",
		70: "Platinum",
	}

	if name, exists := names[occurrence]; exists {
		return name
	}
	return ""
}

func formatOrdinal(n int) string {
	lastTwoDigits := n % 100
	if lastTwoDigits >= 11 && lastTwoDigits <= 13 {
		return fmt.Sprintf("%dth", n)
	}

	switch n % 10 {
	case 1:
		return fmt.Sprintf("%dst", n)
	case 2:
		return fmt.Sprintf("%dnd", n)
	case 3:
		return fmt.Sprintf("%drd", n)
	default:
		return fmt.Sprintf("%dth", n)
	}
}
