package jobs

import (
	"context"
	"fmt"
	"log"
	"os"
	"sort"
	"strings"
	"time"

	"fetcher/internal/clash"
	"fetcher/internal/discord"
)

const (
	clanMonitorInterval = 30 * time.Minute
	offlineThreshold    = 48 * time.Hour
	maxDiscordMessage   = 1900 // leave headroom below Discord's 2000 char limit.
)

func init() {
	job, err := newClanMonitorJobFromEnv()
	if err != nil {
		log.Printf("clan monitor job disabled: %v", err)
		return
	}

	Register(job)
}

// ClanMonitorJob scans clan members and notifies when inactivity exceeds the threshold.
type ClanMonitorJob struct {
	clanTag       string
	recipientID   string
	clashClient   *clash.Client
	discordClient *discord.Client
	notified      map[string]time.Time
}

func newClanMonitorJobFromEnv() (*ClanMonitorJob, error) {
	clanTag := os.Getenv("COC_CLAN_TAG")
	if clanTag == "" {
		return nil, fmt.Errorf("missing COC_CLAN_TAG")
	}

	clashToken := os.Getenv("COC_API_TOKEN")
	if clashToken == "" {
		return nil, fmt.Errorf("missing COC_API_TOKEN")
	}

	discordToken := os.Getenv("DISCORD_BOT_TOKEN")
	if discordToken == "" {
		return nil, fmt.Errorf("missing DISCORD_BOT_TOKEN")
	}

	recipientID := os.Getenv("DISCORD_RECIPIENT_ID")
	if recipientID == "" {
		return nil, fmt.Errorf("missing DISCORD_RECIPIENT_ID")
	}

	return &ClanMonitorJob{
		clanTag:       clanTag,
		recipientID:   recipientID,
		clashClient:   clash.NewClient(clashToken),
		discordClient: discord.NewClient(discordToken),
		notified:      make(map[string]time.Time),
	}, nil
}

func (j *ClanMonitorJob) Name() string {
	return "clan_monitor"
}

func (j *ClanMonitorJob) Interval() time.Duration {
	return clanMonitorInterval
}

func (j *ClanMonitorJob) Run(ctx context.Context) error {
	members, err := j.clashClient.ClanMembers(ctx, j.clanTag)
	if err != nil {
		return fmt.Errorf("fetch clan members: %w", err)
	}

	if len(members) == 0 {
		return nil
	}

	var inactive []inactiveMember
	for _, member := range members {
		if member.LastSeen.IsZero() {
			continue
		}

		inactiveFor := time.Since(member.LastSeen)
		if inactiveFor >= offlineThreshold {
			inactive = append(inactive, inactiveMember{
				MemberDetail: member,
				InactiveFor:  inactiveFor,
			})
		}
	}

	if len(inactive) == 0 {
		return nil
	}

	sort.Slice(inactive, func(i, j int) bool {
		return inactive[i].InactiveFor > inactive[j].InactiveFor
	})

	j.purgeResolved(inactive)

	var newlyInactive []inactiveMember
	for _, member := range inactive {
		if _, alreadyNotified := j.notified[member.Tag]; alreadyNotified {
			continue
		}

		newlyInactive = append(newlyInactive, member)
		j.notified[member.Tag] = time.Now()
		log.Printf("clan monitor: notifying inactivity for %s (%s); offline %s", member.Name, member.Tag, formatDuration(member.InactiveFor))
	}

	if len(newlyInactive) == 0 {
		return nil
	}

	messages := buildMessages(newlyInactive)

	for _, message := range messages {
		if err := j.discordClient.SendDM(ctx, j.recipientID, message); err != nil {
			return fmt.Errorf("send discord notification: %w", err)
		}
	}

	return nil
}

type inactiveMember struct {
	clash.MemberDetail
	InactiveFor time.Duration
}

func (j *ClanMonitorJob) purgeResolved(inactive []inactiveMember) {
	active := make(map[string]struct{}, len(inactive))
	for _, member := range inactive {
		active[member.Tag] = struct{}{}
	}

	for tag := range j.notified {
		if _, stillInactive := active[tag]; !stillInactive {
			delete(j.notified, tag)
		}
	}
}

func buildMessages(members []inactiveMember) []string {
	header := fmt.Sprintf("Clan members inactive ≥ %s\n", formatDuration(offlineThreshold))
	var chunks []string
	var builder strings.Builder

	builder.WriteString(header)

	for _, member := range members {
		entry := formatMember(member)
		if builder.Len()+len(entry) > maxDiscordMessage {
			chunks = append(chunks, builder.String())
			builder.Reset()
			builder.WriteString(header)
		}

		builder.WriteString(entry)
	}

	if builder.Len() > len(header) {
		chunks = append(chunks, builder.String())
	}

	return chunks
}

func formatMember(member inactiveMember) string {
	lastSeen := member.LastSeen.Format(time.RFC3339)
	offline := formatDuration(member.InactiveFor)

	return fmt.Sprintf(
		"• %s (%s)\n  Town Hall: %d | League: %s | Trophies: %d\n  Donated: %d | Received: %d\n  Last Seen: %s (%s ago)\n\n",
		member.Name,
		member.Tag,
		member.TownHallLevel,
		emptyFallback(member.League, "Unknown"),
		member.Trophies,
		member.Donations,
		member.DonationsReceived,
		lastSeen,
		offline,
	)
}

func emptyFallback(value, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return value
}

func formatDuration(d time.Duration) string {
	if d <= 0 {
		return "0s"
	}

	d = d.Round(time.Minute)

	days := d / (24 * time.Hour)
	d -= days * 24 * time.Hour

	hours := d / time.Hour
	d -= hours * time.Hour

	minutes := d / time.Minute

	var parts []string
	if days > 0 {
		parts = append(parts, fmt.Sprintf("%dd", days))
	}
	if hours > 0 {
		parts = append(parts, fmt.Sprintf("%dh", hours))
	}
	if minutes > 0 {
		parts = append(parts, fmt.Sprintf("%dm", minutes))
	}

	return strings.Join(parts, " ")
}
