package emailmonitor

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/mail"
	"strconv"
	"strings"
	"time"

	"email-monitor-app/internal/alert"

	"cloud.google.com/go/pubsub"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/gmail/v1"
	"google.golang.org/api/googleapi"
	"google.golang.org/api/option"
)

// Monitor watches Gmail for sender matches and sends alerts.
type Monitor struct {
	cfg          *Config
	service      *gmail.Service
	webhookURL   string
	state        *State
	statePath    string
	pubsub       *pubsub.Client
	subscription *pubsub.Subscription
	topicName    string
}

// NewMonitor creates a new email monitor instance.
func NewMonitor(cfg *Config) (*Monitor, error) {
	if cfg == nil || !cfg.Enabled {
		return nil, fmt.Errorf("email monitor is not enabled")
	}

	ctx := context.Background()

	var httpClient *http.Client
	if cfg.OAuthRefreshToken != "" {
		oauthCfg := &oauth2.Config{
			ClientID:     cfg.OAuthClientID,
			ClientSecret: cfg.OAuthClientSecret,
			Endpoint:     google.Endpoint,
			Scopes: []string{
				gmail.GmailReadonlyScope,
				gmail.GmailMetadataScope,
			},
		}
		tok := &oauth2.Token{RefreshToken: cfg.OAuthRefreshToken}
		httpClient = oauth2.NewClient(ctx, oauthCfg.TokenSource(ctx, tok))
	} else {
		jwtConfig, err := google.JWTConfigFromJSON(cfg.ServiceJSON,
			gmail.GmailReadonlyScope,
			gmail.GmailMetadataScope,
		)
		if err != nil {
			return nil, fmt.Errorf("failed to parse service account JSON: %w", err)
		}
		jwtConfig.Subject = cfg.Mailbox
		httpClient = jwtConfig.Client(ctx)
	}

	service, err := gmail.NewService(ctx, option.WithHTTPClient(httpClient))
	if err != nil {
		return nil, fmt.Errorf("failed to initialize Gmail service: %w", err)
	}

	state, err := LoadState(cfg.StatePath)
	if err != nil {
		return nil, fmt.Errorf("failed to load email monitor state: %w", err)
	}

	var pubsubClient *pubsub.Client
	var subscription *pubsub.Subscription
	var topicName string
	if cfg.Mode == "watch" {
		pubsubClient, err = pubsub.NewClient(context.Background(), cfg.GCPProject, option.WithCredentialsJSON(cfg.ServiceJSON))
		if err != nil {
			return nil, fmt.Errorf("failed to initialize pubsub client: %w", err)
		}
		subscription = pubsubClient.Subscription(resolveSubscriptionID(cfg.PubSubSubscription))
		topicName = resolveTopicName(cfg.GCPProject, cfg.PubSubTopic)
	}

	return &Monitor{
		cfg:          cfg,
		service:      service,
		webhookURL:   cfg.WebhookURL,
		state:        state,
		statePath:    cfg.StatePath,
		pubsub:       pubsubClient,
		subscription: subscription,
		topicName:    topicName,
	}, nil
}

// Run starts the monitoring loop.
func (m *Monitor) Run(ctx context.Context) {
	if m.cfg.Mode == "watch" {
		log.Printf("Email monitor running in watch mode")
		if err := m.ensureWatch(ctx); err != nil {
			log.Printf("WARN: Failed to establish Gmail watch: %v", err)
		}
		go m.watchRenewLoop(ctx)
		go m.pubsubReceiveLoop(ctx)
	} else {
		log.Printf("Email monitor running in poll mode")
	}

	ticker := time.NewTicker(m.cfg.PollInterval)
	defer ticker.Stop()

	m.runOnce(ctx)

	for {
		select {
		case <-ctx.Done():
			log.Println("Email monitor stopped")
			return
		case <-ticker.C:
			m.runOnce(ctx)
		}
	}
}

func (m *Monitor) runOnce(ctx context.Context) {
	query := m.buildQuery()
	if query == "" {
		log.Println("WARN: Email monitor query is empty; skipping")
		return
	}

	req := m.service.Users.Messages.List("me").Q(query).IncludeSpamTrash(false)

	var latestAfter int64 = m.state.LastAfter

	for {
		resp, err := req.Context(ctx).Do()
		if err != nil {
			log.Printf("WARN: Email monitor list failed: %v", err)
			return
		}

		for _, msg := range resp.Messages {
			m.processMessage(ctx, msg.Id, &latestAfter)
		}

		if resp.NextPageToken == "" {
			break
		}
		req.PageToken(resp.NextPageToken)
	}

	if latestAfter > m.state.LastAfter {
		m.state.LastAfter = latestAfter
	}

	m.state.Prune(m.cfg.HistoryWindow)
	if err := m.state.Save(m.statePath); err != nil {
		log.Printf("WARN: Failed to save email monitor state: %v", err)
	}
}

func (m *Monitor) processMessage(ctx context.Context, messageID string, latestAfter *int64) {
	if messageID == "" {
		return
	}
	if m.state.SeenMessage(messageID) {
		return
	}

	msg, err := m.service.Users.Messages.Get("me", messageID).
		Format("metadata").
		MetadataHeaders("From", "To", "Subject", "Date").
		Context(ctx).
		Do()
	if err != nil {
		log.Printf("WARN: Email monitor failed to fetch message %s: %v", messageID, err)
		return
	}
	if msg.Payload == nil {
		log.Printf("WARN: Email monitor received empty payload for message %s", messageID)
		return
	}

	from := getHeaderValue(msg.Payload.Headers, "From")
	sender := extractEmail(from)
	if !m.senderAllowed(sender) {
		return
	}

	subject := getHeaderValue(msg.Payload.Headers, "Subject")
	date := getHeaderValue(msg.Payload.Headers, "Date")
	snippet := msg.Snippet

	internalDate := time.Now()
	if msg.InternalDate > 0 {
		internalDate = time.UnixMilli(msg.InternalDate)
	}

	alertMessage := strings.Builder{}
	alertMessage.WriteString("Email detected\n")
	alertMessage.WriteString(fmt.Sprintf("Mailbox: %s\n", m.cfg.Mailbox))
	alertMessage.WriteString(fmt.Sprintf("From: %s\n", from))
	if subject != "" {
		alertMessage.WriteString(fmt.Sprintf("Subject: %s\n", subject))
	}
	if date != "" {
		alertMessage.WriteString(fmt.Sprintf("Date: %s\n", date))
	}
	if snippet != "" {
		alertMessage.WriteString(fmt.Sprintf("Snippet: %s\n", snippet))
	}
	alertMessage.WriteString(fmt.Sprintf("Message ID: %s", messageID))

	if err := alert.SendText(m.webhookURL, alertMessage.String()); err != nil {
		log.Printf("WARN: Email monitor failed to send alert: %v", err)
		return
	}

	m.state.MarkSeen(messageID, internalDate)
	if latestAfter != nil && internalDate.Unix() > *latestAfter {
		*latestAfter = internalDate.Unix()
	}
}

func (m *Monitor) pubsubReceiveLoop(ctx context.Context) {
	if m.subscription == nil {
		return
	}
	m.subscription.ReceiveSettings.MaxOutstandingMessages = 20
	m.subscription.ReceiveSettings.NumGoroutines = 4

	err := m.subscription.Receive(ctx, func(ctx context.Context, msg *pubsub.Message) {
		if err := m.handlePubSubMessage(ctx, msg.Data); err != nil {
			log.Printf("WARN: Failed to handle pubsub message: %v", err)
			msg.Nack()
			return
		}
		msg.Ack()
	})
	if err != nil && ctx.Err() == nil {
		log.Printf("WARN: Pub/Sub receive loop exited: %v", err)
	}
}

func (m *Monitor) handlePubSubMessage(ctx context.Context, data []byte) error {
	var payload struct {
		EmailAddress string `json:"emailAddress"`
		HistoryID    string `json:"historyId"`
	}
	if err := json.Unmarshal(data, &payload); err != nil {
		return fmt.Errorf("invalid pubsub payload: %w", err)
	}
	if payload.HistoryID == "" {
		return fmt.Errorf("pubsub payload missing historyId")
	}

	historyID, err := strconv.ParseUint(payload.HistoryID, 10, 64)
	if err != nil {
		return fmt.Errorf("invalid historyId: %w", err)
	}

	if m.state.LastHistoryID == 0 {
		m.state.LastHistoryID = historyID
		m.state.LastWatchTime = time.Now().Unix()
		return m.state.Save(m.statePath)
	}

	return m.processHistory(ctx, historyID)
}

func (m *Monitor) processHistory(ctx context.Context, latestHistoryID uint64) error {
	startID := m.state.LastHistoryID
	if startID == 0 {
		m.state.LastHistoryID = latestHistoryID
		return nil
	}

	historyCall := m.service.Users.History.List("me").StartHistoryId(startID)
	resp, err := historyCall.Context(ctx).Do()
	if err != nil {
		if isHistoryExpired(err) {
			log.Printf("WARN: Gmail historyId expired; resetting watch")
			return m.resetWatch(ctx)
		}
		return fmt.Errorf("history list failed: %w", err)
	}

	messageIDs := make(map[string]struct{})
	for _, entry := range resp.History {
		for _, added := range entry.MessagesAdded {
			if added != nil && added.Message != nil && added.Message.Id != "" {
				messageIDs[added.Message.Id] = struct{}{}
			}
		}
	}

	for messageID := range messageIDs {
		m.processMessage(ctx, messageID, nil)
	}

	m.state.LastHistoryID = latestHistoryID
	m.state.Prune(m.cfg.HistoryWindow)
	if err := m.state.Save(m.statePath); err != nil {
		log.Printf("WARN: Failed to save email monitor state: %v", err)
	}
	return nil
}

func (m *Monitor) watchRenewLoop(ctx context.Context) {
	ticker := time.NewTicker(m.cfg.WatchRenewal)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			if err := m.ensureWatch(ctx); err != nil {
				log.Printf("WARN: Failed to renew Gmail watch: %v", err)
			}
		}
	}
}

func (m *Monitor) ensureWatch(ctx context.Context) error {
	if m.cfg.Mode != "watch" {
		return nil
	}
	if m.topicName == "" {
		return fmt.Errorf("watch topic is not configured")
	}

	req := &gmail.WatchRequest{
		TopicName: m.topicName,
		LabelIds:  []string{"INBOX"},
	}
	resp, err := m.service.Users.Watch("me", req).Context(ctx).Do()
	if err != nil {
		return err
	}

	if resp.HistoryId != 0 {
		m.state.LastHistoryID = resp.HistoryId
	}
	m.state.LastWatchTime = time.Now().Unix()
	return m.state.Save(m.statePath)
}

func (m *Monitor) resetWatch(ctx context.Context) error {
	if err := m.ensureWatch(ctx); err != nil {
		return err
	}
	return nil
}

func resolveTopicName(project, topic string) string {
	if strings.HasPrefix(topic, "projects/") {
		return topic
	}
	return fmt.Sprintf("projects/%s/topics/%s", project, topic)
}

func resolveSubscriptionID(subscription string) string {
	if strings.HasPrefix(subscription, "projects/") {
		parts := strings.Split(subscription, "/subscriptions/")
		if len(parts) == 2 {
			return parts[1]
		}
	}
	return subscription
}

func isHistoryExpired(err error) bool {
	apiErr, ok := err.(*googleapi.Error)
	if !ok {
		return false
	}
	if apiErr.Code == 404 {
		return true
	}
	for _, detail := range apiErr.Errors {
		if strings.Contains(strings.ToLower(detail.Message), "history") && strings.Contains(strings.ToLower(detail.Message), "not found") {
			return true
		}
	}
	return false
}

func (m *Monitor) buildQuery() string {
	if len(m.cfg.Senders) == 0 {
		return ""
	}

	clauses := make([]string, 0, len(m.cfg.Senders))
	for _, sender := range m.cfg.Senders {
		clauses = append(clauses, fmt.Sprintf("from:%s", sender))
	}

	senderQuery := strings.Join(clauses, " OR ")
	after := m.state.LastAfter
	if after == 0 {
		after = time.Now().Add(-m.cfg.HistoryWindow).Unix()
	}
	return fmt.Sprintf("(%s) after:%d", senderQuery, after)
}

func getHeaderValue(headers []*gmail.MessagePartHeader, name string) string {
	name = strings.ToLower(name)
	for _, header := range headers {
		if strings.ToLower(header.Name) == name {
			return header.Value
		}
	}
	return ""
}

func extractEmail(from string) string {
	addr, err := mail.ParseAddress(from)
	if err != nil {
		return strings.ToLower(strings.TrimSpace(from))
	}
	return strings.ToLower(addr.Address)
}

func (m *Monitor) senderAllowed(sender string) bool {
	if sender == "" {
		return false
	}
	for _, allowed := range m.cfg.Senders {
		if strings.EqualFold(allowed, sender) {
			return true
		}
	}
	return false
}
