package bot

import (
	"context"
	"fmt"
	"strings"
	"time"

	"gitlab.com/etke.cc/linkpearl"
	"maunium.net/go/mautrix/event"
	"maunium.net/go/mautrix/format"
	"maunium.net/go/mautrix/id"

	"gitlab.com/etke.cc/postmoogle/bot/config"
	"gitlab.com/etke.cc/postmoogle/email"
	"gitlab.com/etke.cc/postmoogle/utils"
)

const (
	commandHelp           = "help"
	commandStop           = "stop"
	commandSend           = "send"
	commandDKIM           = "dkim"
	commandCatchAll       = config.BotCatchAll
	commandUsers          = config.BotUsers
	commandQueueBatch     = config.BotQueueBatch
	commandQueueRetries   = config.BotQueueRetries
	commandSpamlist       = "spam:list"
	commandSpamlistAdd    = "spam:add"
	commandSpamlistRemove = "spam:remove"
	commandSpamlistReset  = "spam:reset"
	commandDelete         = "delete"
	commandBanlist        = "banlist"
	commandBanlistTotals  = "banlist:totals"
	commandBanlistAuto    = "banlist:auto"
	commandBanlistAuth    = "banlist:auth"
	commandBanlistAdd     = "banlist:add"
	commandBanlistRemove  = "banlist:remove"
	commandBanlistReset   = "banlist:reset"
	commandMailboxes      = "mailboxes"
)

type (
	command struct {
		key         string
		description string
		sanitizer   func(string) string
		allowed     func(id.UserID, id.RoomID) bool
	}
	commandList []command
)

func (c commandList) get(key string) *command {
	for _, cmd := range c {
		if cmd.key == key {
			return &cmd
		}
	}
	return nil
}

func (b *Bot) initCommands() commandList {
	return commandList{
		// special commands
		{
			key:         commandHelp,
			description: "Show this help message",
			allowed:     b.allowAnyone,
		},
		{
			key:         commandStop,
			description: "Disable bridge for the room and clear all configuration",
			allowed:     b.allowOwner,
		},
		{
			key:         commandSend,
			description: "Send email",
			allowed:     b.allowSend,
		},
		{allowed: b.allowOwner, description: "mailbox ownership"}, // delimiter
		// options commands
		{
			key:         config.RoomMailbox,
			description: "Get or set mailbox of the room",
			sanitizer:   utils.Mailbox,
			allowed:     b.allowOwner,
		},
		{
			key:         config.RoomDomain,
			description: "Get or set default domain of the room",
			sanitizer:   utils.SanitizeDomain,
			allowed:     b.allowOwner,
		},
		{
			key:         config.RoomOwner,
			description: "Get or set owner of the room",
			sanitizer:   func(s string) string { return s },
			allowed:     b.allowOwner,
		},
		{
			key:         config.RoomPassword,
			description: "Get or set SMTP password of the room's mailbox",
			allowed:     b.allowOwner,
		},
		{allowed: b.allowOwner, description: "mailbox options"}, // delimiter
		{
			key:         config.RoomAutoreply,
			description: "Get or set autoreply of the room (markdown supported) that will be send for any new incoming email thread",
			sanitizer:   func(s string) string { return s },
			allowed:     b.allowOwner,
		},
		{
			key:         config.RoomSignature,
			description: "Get or set signature of the room (markdown supported)",
			sanitizer:   func(s string) string { return s },
			allowed:     b.allowOwner,
		},
		{
			key: config.RoomNoSend,
			description: fmt.Sprintf(
				"Get or set `%s` of the room (`true` - disable email sending; `false` - enable email sending)",
				config.RoomNoSend,
			),
			sanitizer: utils.SanitizeBoolString,
			allowed:   b.allowOwner,
		},
		{
			key: config.RoomNoReplies,
			description: fmt.Sprintf(
				"Get or set `%s` of the room (`true` - ignore matrix replies; `false` - parse matrix replies)",
				config.RoomNoReplies,
			),
			sanitizer: utils.SanitizeBoolString,
			allowed:   b.allowOwner,
		},
		{
			key: config.RoomNoSender,
			description: fmt.Sprintf(
				"Get or set `%s` of the room (`true` - hide email sender; `false` - show email sender)",
				config.RoomNoSender,
			),
			sanitizer: utils.SanitizeBoolString,
			allowed:   b.allowOwner,
		},
		{
			key: config.RoomNoRecipient,
			description: fmt.Sprintf(
				"Get or set `%s` of the room (`true` - hide recipient; `false` - show recipient)",
				config.RoomNoRecipient,
			),
			sanitizer: utils.SanitizeBoolString,
			allowed:   b.allowOwner,
		},
		{
			key: config.RoomNoCC,
			description: fmt.Sprintf(
				"Get or set `%s` of the room (`true` - hide CC; `false` - show CC)",
				config.RoomNoCC,
			),
			sanitizer: utils.SanitizeBoolString,
			allowed:   b.allowOwner,
		},
		{
			key: config.RoomNoSubject,
			description: fmt.Sprintf(
				"Get or set `%s` of the room (`true` - hide email subject; `false` - show email subject)",
				config.RoomNoSubject,
			),
			sanitizer: utils.SanitizeBoolString,
			allowed:   b.allowOwner,
		},
		{
			key: config.RoomNoHTML,
			description: fmt.Sprintf(
				"Get or set `%s` of the room (`true` - ignore HTML in email; `false` - parse HTML in emails)",
				config.RoomNoHTML,
			),
			sanitizer: utils.SanitizeBoolString,
			allowed:   b.allowOwner,
		},
		{
			key: config.RoomNoThreads,
			description: fmt.Sprintf(
				"Get or set `%s` of the room (`true` - ignore email threads; `false` - convert email threads into matrix threads)",
				config.RoomNoThreads,
			),
			sanitizer: utils.SanitizeBoolString,
			allowed:   b.allowOwner,
		},
		{
			key: config.RoomNoFiles,
			description: fmt.Sprintf(
				"Get or set `%s` of the room (`true` - ignore email attachments; `false` - upload email attachments)",
				config.RoomNoFiles,
			),
			sanitizer: utils.SanitizeBoolString,
			allowed:   b.allowOwner,
		},
		{
			key: config.RoomNoInlines,
			description: fmt.Sprintf(
				"Get or set `%s` of the room (`true` - ignore inline attachments; `false` - upload inline attachments)",
				config.RoomNoInlines,
			),
			sanitizer: utils.SanitizeBoolString,
			allowed:   b.allowOwner,
		},
		{allowed: b.allowOwner, description: "mailbox security checks"}, // delimiter
		{
			key:         config.RoomSpamcheckMX,
			description: "only accept email from servers which seem prepared to receive it (those having valid MX records) (`true` - enable, `false` - disable)",
			sanitizer:   utils.SanitizeBoolString,
			allowed:     b.allowOwner,
		},
		{
			key:         config.RoomSpamcheckSPF,
			description: "only accept email from senders which authorized to send it (those matching SPF records) (`true` - enable, `false` - disable)",
			sanitizer:   utils.SanitizeBoolString,
			allowed:     b.allowOwner,
		},
		{
			key:         config.RoomSpamcheckDKIM,
			description: "only accept correctly authorized emails (without DKIM signature at all or with valid DKIM signature) (`true` - enable, `false` - disable)",
			sanitizer:   utils.SanitizeBoolString,
			allowed:     b.allowOwner,
		},
		{
			key:         config.RoomSpamcheckSMTP,
			description: "only accept email from servers which seem prepared to receive it (those listening on an SMTP port) (`true` - enable, `false` - disable)",
			sanitizer:   utils.SanitizeBoolString,
			allowed:     b.allowOwner,
		},
		{allowed: b.allowOwner, description: "mailbox anti-spam"}, // delimiter
		{
			key:         commandSpamlist,
			description: "Show comma-separated spamlist of the room, eg: `spammer@example.com,*@spammer.org,spam@*`",
			sanitizer:   utils.SanitizeStringSlice,
			allowed:     b.allowOwner,
		},
		{
			key:         commandSpamlistAdd,
			description: "Mark an email address (or pattern) as spam (or you can react to the email with emoji: ⛔️,🛑, or 🚫)",
			allowed:     b.allowOwner,
		},
		{
			key:         commandSpamlistRemove,
			description: "Unmark an email address (or pattern) as spam",
			allowed:     b.allowOwner,
		},
		{
			key:         commandSpamlistReset,
			description: "Reset spamlist",
			allowed:     b.allowOwner,
		},
		{allowed: b.allowAdmin, description: "server options"}, // delimiter
		{
			key:         config.BotAdminRoom,
			description: "Get or set admin room",
			allowed:     b.allowAdmin,
		},
		{
			key:         config.BotUsers,
			description: "Get or set allowed users",
			allowed:     b.allowAdmin,
		},
		{
			key:         commandDKIM,
			description: "Get DKIM signature",
			allowed:     b.allowAdmin,
		},
		{
			key:         commandCatchAll,
			description: "Get or set catch-all mailbox",
			allowed:     b.allowAdmin,
		},
		{
			key:         commandQueueBatch,
			description: "max amount of emails to process on each queue check",
			sanitizer:   utils.SanitizeIntString,
			allowed:     b.allowAdmin,
		},
		{
			key:         commandQueueRetries,
			description: "max amount of tries per email in queue before removal",
			sanitizer:   utils.SanitizeIntString,
			allowed:     b.allowAdmin,
		},
		{
			key:         commandMailboxes,
			description: "Show the list of all mailboxes",
			allowed:     b.allowAdmin,
		},
		{
			key:         commandDelete,
			description: "Delete specific mailbox",
			allowed:     b.allowAdmin,
		},
		{allowed: b.allowAdmin, description: "server antispam"}, // delimiter
		{
			key:         config.BotGreylist,
			description: "Set automatic greylisting duration in minutes (0 - disabled)",
			allowed:     b.allowAdmin,
		},
		{
			key:         commandBanlist,
			description: "Enable/disable banlist and show current values",
			allowed:     b.allowAdmin,
		},
		{
			key:         commandBanlistAuth,
			description: "Enable/disable automatic banning of IP addresses when they try to auth with invalid credentials",
			allowed:     b.allowAdmin,
		},
		{
			key:         commandBanlistAuto,
			description: "Enable/disable automatic banning of IP addresses when they try to send invalid emails",
			allowed:     b.allowAdmin,
		},
		{
			key:         commandBanlistTotals,
			description: "List banlist totals only",
			allowed:     b.allowAdmin,
		},
		{
			key:         commandBanlistAdd,
			description: "Ban an IP",
			allowed:     b.allowAdmin,
		},
		{
			key:         commandBanlistRemove,
			description: "Unban an IP",
			allowed:     b.allowAdmin,
		},
		{
			key:         commandBanlistReset,
			description: "Reset banlist",
			allowed:     b.allowAdmin,
		},
	}
}

func (b *Bot) handle(ctx context.Context) {
	evt := eventFromContext(ctx)
	err := b.lp.GetClient().MarkRead(evt.RoomID, evt.ID)
	if err != nil {
		b.log.Error().Err(err).Msg("cannot send read receipt")
	}

	content := evt.Content.AsMessage()
	if content == nil {
		b.Error(ctx, "cannot read message")
		return
	}
	// ignore notices
	if content.MsgType == event.MsgNotice {
		return
	}
	message := strings.TrimSpace(content.Body)
	commandSlice := b.parseCommand(message, true)
	if commandSlice == nil {
		if linkpearl.EventParent("", content) != "" {
			b.SendEmailReply(ctx)
		}
		return
	}

	cmd := b.commands.get(commandSlice[0])
	if cmd == nil {
		return
	}
	_, err = b.lp.GetClient().UserTyping(evt.RoomID, true, 30*time.Second)
	if err != nil {
		b.log.Error().Err(err).Msg("cannot send typing notification")
	}
	defer b.lp.GetClient().UserTyping(evt.RoomID, false, 30*time.Second) //nolint:errcheck

	if !cmd.allowed(evt.Sender, evt.RoomID) {
		b.lp.SendNotice(evt.RoomID, "not allowed to do that, kupo")
		return
	}

	switch commandSlice[0] {
	case commandHelp:
		b.sendHelp(ctx)
	case commandStop:
		b.runStop(ctx)
	case commandSend:
		b.runSend(ctx)
	case commandDKIM:
		b.runDKIM(ctx, commandSlice)
	case commandSpamlistAdd:
		b.runSpamlistAdd(ctx, commandSlice)
	case commandSpamlistRemove:
		b.runSpamlistRemove(ctx, commandSlice)
	case commandSpamlistReset:
		b.runSpamlistReset(ctx)
	case config.BotAdminRoom:
		b.runAdminRoom(ctx, commandSlice)
	case commandUsers:
		b.runUsers(ctx, commandSlice)
	case commandCatchAll:
		b.runCatchAll(ctx, commandSlice)
	case commandDelete:
		b.runDelete(ctx, commandSlice)
	case config.BotGreylist:
		b.runGreylist(ctx, commandSlice)
	case commandBanlist:
		b.runBanlist(ctx, commandSlice)
	case commandBanlistAuth:
		b.runBanlistAuth(ctx, commandSlice)
	case commandBanlistAuto:
		b.runBanlistAuto(ctx, commandSlice)
	case commandBanlistTotals:
		b.runBanlistTotals(ctx)
	case commandBanlistAdd:
		b.runBanlistAdd(ctx, commandSlice)
	case commandBanlistRemove:
		b.runBanlistRemove(ctx, commandSlice)
	case commandBanlistReset:
		b.runBanlistReset(ctx)
	case commandMailboxes:
		b.sendMailboxes(ctx)
	default:
		b.handleOption(ctx, commandSlice)
	}
}

func (b *Bot) parseCommand(message string, toLower bool) []string {
	if message == "" {
		return nil
	}

	index := strings.LastIndex(message, b.prefix)
	if index == -1 {
		return nil
	}

	message = strings.Replace(message, b.prefix, "", 1)
	if toLower {
		message = strings.ToLower(message)
	}
	return strings.Split(strings.TrimSpace(message), " ")
}

func (b *Bot) sendIntroduction(roomID id.RoomID) {
	var msg strings.Builder
	msg.WriteString("Hello, kupo!\n\n")

	msg.WriteString("This is Postmoogle - a bot that bridges Email to Matrix.\n\n")

	msg.WriteString("To get started, assign an email address to this room by sending a `")
	msg.WriteString(b.prefix)
	msg.WriteString(" ")
	msg.WriteString(config.RoomMailbox)
	msg.WriteString(" SOME_INBOX` command.\n")

	msg.WriteString("You will then be able to send emails to ")
	msg.WriteString(utils.EmailsList("SOME_INBOX", ""))
	msg.WriteString("` and have them appear in this room.")

	b.lp.SendNotice(roomID, msg.String())
}

func (b *Bot) getHelpValue(cfg config.Room, cmd command) string {
	name := cmd.key
	if name == commandSpamlist {
		name = config.RoomSpamlist
	}

	value := cfg.Get(name)
	if cmd.sanitizer != nil {
		switch value != "" {
		case false:
			return "(currently not set)"
		case true:
			txt := "(currently " + value
			if cmd.key == config.RoomMailbox {
				txt += " (" + utils.EmailsList(value, cfg.Domain()) + ")"
			}
			return txt + ")"
		}
	}

	return ""
}

func (b *Bot) sendHelp(ctx context.Context) {
	evt := eventFromContext(ctx)

	cfg, serr := b.cfg.GetRoom(evt.RoomID)
	if serr != nil {
		b.log.Error().Err(serr).Msg("cannot retrieve settings")
	}

	var msg strings.Builder
	msg.WriteString("The following commands are supported and accessible to you:\n\n")
	for _, cmd := range b.commands {
		if !cmd.allowed(evt.Sender, evt.RoomID) {
			continue
		}
		if cmd.key == "" {
			msg.WriteString("\n---\n\n")
			msg.WriteString("#### ")
			msg.WriteString(cmd.description)
			msg.WriteString("\n")
			continue
		}
		msg.WriteString("* **`")
		msg.WriteString(b.prefix)
		msg.WriteString(" ")
		msg.WriteString(cmd.key)
		msg.WriteString("`**")

		msg.WriteString(b.getHelpValue(cfg, cmd))
		msg.WriteString(" - ")

		msg.WriteString(cmd.description)
		msg.WriteString("\n")
	}

	b.lp.SendNotice(evt.RoomID, msg.String(), linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
}

func (b *Bot) runSend(ctx context.Context) {
	evt := eventFromContext(ctx)
	to, subject, body, shouldSend := b.getSendDetails(ctx)
	if !shouldSend {
		return
	}

	cfg, err := b.cfg.GetRoom(evt.RoomID)
	if err != nil {
		b.Error(ctx, "failed to retrieve room settings: %v", err)
		return
	}

	var htmlBody string
	if !cfg.NoHTML() {
		htmlBody = format.RenderMarkdown(body, true, true).FormattedBody
	}

	tos := strings.Split(to, ",")
	b.runSendCommand(ctx, cfg, tos, subject, body, htmlBody)
}

func (b *Bot) getSendDetails(ctx context.Context) (string, string, string, bool) {
	evt := eventFromContext(ctx)
	if !b.allowSend(evt.Sender, evt.RoomID) {
		return "", "", "", false
	}

	cfg, err := b.cfg.GetRoom(evt.RoomID)
	if err != nil {
		b.Error(ctx, "failed to retrieve room settings: %v", err)
		return "", "", "", false
	}

	commandSlice := b.parseCommand(evt.Content.AsMessage().Body, false)
	to, subject, body, err := utils.ParseSend(commandSlice)
	if err == utils.ErrInvalidArgs {
		b.lp.SendNotice(evt.RoomID, fmt.Sprintf(
			"Usage:\n"+
				"```\n"+
				"%s send someone@example.com\n"+
				"Subject goes here on a line of its own\n"+
				"Email content goes here\n"+
				"on as many lines\n"+
				"as you want.\n"+
				"```",
			b.prefix),
			linkpearl.RelatesTo(evt.ID, cfg.NoThreads()),
		)
		return "", "", "", false
	}

	mailbox := cfg.Mailbox()
	if mailbox == "" {
		b.lp.SendNotice(evt.RoomID, "mailbox is not configured, kupo", linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
		return "", "", "", false
	}

	signature := cfg.Signature()
	if signature != "" {
		body += "\n\n---\n" + signature
	}

	return to, subject, body, true
}

func (b *Bot) runSendCommand(ctx context.Context, cfg config.Room, tos []string, subject, body, htmlBody string) {
	evt := eventFromContext(ctx)

	// validate first
	for _, to := range tos {
		if !email.AddressValid(to) {
			b.Error(ctx, "email address is not valid")
			return
		}
	}

	b.mu.Lock(evt.RoomID.String())
	defer b.mu.Unlock(evt.RoomID.String())

	domain := utils.SanitizeDomain(cfg.Domain())
	from := cfg.Mailbox() + "@" + domain
	ID := email.MessageID(evt.ID, domain)
	for _, to := range tos {
		recipients := []string{to}
		eml := email.New(ID, "", " "+ID, subject, from, to, to, "", body, htmlBody, nil, nil)
		data := eml.Compose(b.cfg.GetBot().DKIMPrivateKey())
		if data == "" {
			b.lp.SendNotice(evt.RoomID, "email body is empty", linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
			return
		}
		queued, err := b.Sendmail(evt.ID, from, to, data)
		if queued {
			b.log.Warn().Err(err).Msg("email has been queued")
			b.saveSentMetadata(ctx, queued, evt.ID, recipients, eml, cfg)
			continue
		}
		if err != nil {
			b.Error(ctx, "cannot send email to %s: %v", to, err)
			continue
		}
		b.saveSentMetadata(ctx, false, evt.ID, recipients, eml, cfg)
	}
	if len(tos) > 1 {
		b.lp.SendNotice(evt.RoomID, "All emails were sent.", linkpearl.RelatesTo(evt.ID, cfg.NoThreads()))
	}
}
