// Package imap provides an IMAP client wrapper for monitoring and fetching
// email messages. It handles connection management, IDLE support for push
// notifications, and message operations like fetching and marking as seen.
//
// The client supports server-side filtering via [filter.Filters] to limit
// which messages are returned by [Client.Unseen].
package imap

import (
	"context"
	"fmt"
	"log/slog"
	"time"

	"code.chimeric.al/chimerical/raven/internal/filter"

	"github.com/emersion/go-imap/v2"
	"github.com/emersion/go-imap/v2/imapclient"
)

// Config holds IMAP server connection parameters.
type Config struct {
	// Host is the IMAP server hostname.
	Host string `yaml:"host"`
	// Password is the authentication password or app-specific password.
	Password string `yaml:"password"`
	// Port is the IMAP server port (typically "993" for TLS).
	Port string `yaml:"port"`
	// User is the authentication username, usually an email address.
	User string `yaml:"user"`
}

// Address returns the host:port string for dialing.
func (c *Config) Address() string {
	return fmt.Sprintf("%s:%s", c.Host, c.Port)
}

// Validate checks that all required fields are present.
func (c *Config) Validate() error {
	if c.Host == "" {
		return fmt.Errorf("missing host")
	}
	if c.Password == "" {
		return fmt.Errorf("missing password")
	}
	if c.Port == "" {
		return fmt.Errorf("missing port")
	}
	if c.User == "" {
		return fmt.Errorf("missing user")
	}

	return nil
}

// Client wraps an IMAP client with connection management and filtering.
type Client struct {
	cfg     Config
	filters filter.Filters
	client  *imapclient.Client
	log     *slog.Logger
}

// NewClient creates a new Client with the given configuration.
// The client is not connected until [Client.Connect] is called.
func NewClient(
	cfg Config,
	filters filter.Filters,
	log *slog.Logger,
) (*Client, error) {
	if err := cfg.Validate(); err != nil {
		return nil, fmt.Errorf("invalid config: %v", err)
	}
	return &Client{
		cfg:     cfg,
		filters: filters,
		log:     log,
	}, nil
}

// Client returns the underlying imapclient.Client.
// Returns nil if not connected.
func (c *Client) Client() *imapclient.Client {
	return c.client
}

// Connect establishes a TLS connection to the IMAP server,
// authenticates, and selects the INBOX.
func (c *Client) Connect(
	ctx context.Context,
	log *slog.Logger,
	opts *imapclient.Options,
) error {
	c.log = log

	c.log.InfoContext(ctx, "connecting to IMAP server")
	client, err := imapclient.DialTLS(c.cfg.Address(), opts)
	if err != nil {
		return fmt.Errorf("dial TLS: %w", err)
	}
	c.client = client
	c.log.InfoContext(ctx, "connected to IMAP server")

	c.log.InfoContext(ctx, "logging into to IMAP server")
	if err := client.Login(c.cfg.User, c.cfg.Password).Wait(); err != nil {
		return fmt.Errorf("login: %w", err)
	}
	c.log.InfoContext(ctx, "logged into IMAP server")

	c.log.InfoContext(ctx, "selecting INBOX")
	if _, err := client.Select("INBOX", nil).Wait(); err != nil {
		return fmt.Errorf("select: %w", err)
	}
	c.log.InfoContext(ctx, "selected INBOX")

	return nil
}

// CanIdle reports whether the server advertises IDLE capability (RFC 2177).
func (c *Client) CanIdle() bool {
	return c.client.Caps().Has(imap.CapIdle)
}

// Disconnect logs out from the server and closes the connection.
// Safe to call on a nil or disconnected client.
func (c *Client) Disconnect(ctx context.Context) {
	if c.client == nil {
		return
	}

	c.log.InfoContext(ctx, "logging out")
	ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel()

	var done = make(chan error, 1)
	go func() {
		done <- c.client.Logout().Wait()
	}()
	select {
	case <-ctx.Done():
		c.log.InfoContext(
			ctx,
			"context closed",
			slog.Any("reason", ctx.Err()),
		)

	case err := <-done:
		if err != nil {
			c.log.Warn(
				"failed to logout from IMAP server",
				slog.Any("error", err),
			)
		}
	}

	c.client.Close()
	c.client = nil
}

// Fetch retrieves a message by UID. If peek is true, the message is
// fetched without marking it as seen (using BODY.PEEK[]).
func (c *Client) Fetch(uid imap.UID, peek bool) (
	*imapclient.FetchMessageBuffer, error,
) {
	if c.client == nil {
		return nil, fmt.Errorf("client not connected")
	}

	opts := &imap.FetchOptions{Envelope: true, UID: true}
	if peek {
		opts.BodySection = []*imap.FetchItemBodySection{{Peek: true}}
	}

	res, err := c.client.Fetch(imap.UIDSetNum(uid), opts).Collect()
	if err != nil {
		return nil, fmt.Errorf("fetch: %w", err)
	}
	if len(res) == 0 {
		return nil, fmt.Errorf("message not found")
	}

	return res[0], nil
}

// Idle starts an IDLE command (RFC 2177) to wait for server notifications.
// The caller must call Close on the returned command to end IDLE mode.
func (c *Client) Idle() (*imapclient.IdleCommand, error) {
	return c.client.Idle()
}

// MarkSeen adds the \Seen flag to the message with the given UID.
func (c *Client) MarkSeen(uid imap.UID) error {
	if c.client == nil {
		return fmt.Errorf("client not connected")
	}

	if _, err := c.client.Store(
		imap.UIDSetNum(uid),
		&imap.StoreFlags{
			Op:     imap.StoreFlagsAdd,
			Silent: true,
			Flags:  []imap.Flag{imap.FlagSeen},
		},
		nil,
	).Collect(); err != nil {
		return fmt.Errorf("collect: %w", err)
	}

	return nil
}

// Noop sends a NOOP command to refresh mailbox state.
func (c *Client) Noop() error {
	if c.client == nil {
		return fmt.Errorf("client not connected")
	}
	return c.client.Noop().Wait()
}

// Unseen returns UIDs of messages matching the client's filters that
// do not have the \Seen flag. Filters are applied server-side.
func (c *Client) Unseen(ctx context.Context) ([]imap.UID, error) {
	sc := &imap.SearchCriteria{
		NotFlag: []imap.Flag{imap.FlagSeen},
	}
	if len(c.filters.Body) > 0 {
		sc.Body = c.filters.Body
	}
	if c.filters.From != "" {
		sc.Header = append(
			sc.Header,
			imap.SearchCriteriaHeaderField{
				Key:   "From",
				Value: c.filters.From,
			},
		)
	}
	if c.filters.Subject != "" {
		sc.Header = append(
			sc.Header,
			imap.SearchCriteriaHeaderField{
				Key:   "Subject",
				Value: c.filters.Subject,
			},
		)
	}
	if c.filters.To != "" {
		sc.Header = append(
			sc.Header,
			imap.SearchCriteriaHeaderField{
				Key:   "To",
				Value: c.filters.To,
			},
		)
	}

	res, err := c.client.UIDSearch(sc, nil).Wait()
	if err != nil {
		return nil, fmt.Errorf("search: %v", err)
	}
	uids := res.AllUIDs()

	return uids, nil
}
