package plugin

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"net/url"
	"os"
	"strings"
	"sync"
	"time"

	"gogobee/internal/db"

	"maunium.net/go/mautrix"
	"maunium.net/go/mautrix/id"
)

// bandsintownEvent represents an event from the Bandsintown API.
type bandsintownEvent struct {
	ID       string `json:"id"`
	URL      string `json:"url"`
	DateTime string `json:"datetime"`
	Title    string `json:"title"`
	Venue    struct {
		Name      string `json:"name"`
		City      string `json:"city"`
		Region    string `json:"region"`
		Country   string `json:"country"`
		Latitude  string `json:"latitude"`
		Longitude string `json:"longitude"`
	} `json:"venue"`
	Lineup   []string `json:"lineup"`
	OnSaleAt string   `json:"on_sale_datetime"`
	Offers   []struct {
		Type   string `json:"type"`
		URL    string `json:"url"`
		Status string `json:"status"`
	} `json:"offers"`
}

// ConcertsPlugin provides concert lookups via Bandsintown.
type ConcertsPlugin struct {
	Base
	apiKey      string
	httpClient  *http.Client
	rateLimiter *RateLimitsPlugin
	mu          sync.Mutex
	cooldowns   map[string]time.Time // keyed by userID+artist
}

// NewConcertsPlugin creates a new ConcertsPlugin.
func NewConcertsPlugin(client *mautrix.Client, rateLimiter *RateLimitsPlugin) *ConcertsPlugin {
	return &ConcertsPlugin{
		Base:        NewBase(client),
		apiKey:      os.Getenv("BANDSINTOWN_API_KEY"),
		rateLimiter: rateLimiter,
		httpClient: &http.Client{
			Timeout: 10 * time.Second,
		},
		cooldowns: make(map[string]time.Time),
	}
}

func (p *ConcertsPlugin) Name() string { return "concerts" }

func (p *ConcertsPlugin) Commands() []CommandDef {
	return []CommandDef{
		{Name: "concerts", Description: "Search upcoming concerts for an artist", Usage: "!concerts <artist>", Category: "Entertainment"},
		{Name: "concerts watch", Description: "Watch an artist for concert updates", Usage: "!concerts watch <artist>", Category: "Entertainment"},
		{Name: "concerts watching", Description: "List your watched artists", Usage: "!concerts watching", Category: "Entertainment"},
		{Name: "concerts unwatch", Description: "Stop watching an artist", Usage: "!concerts unwatch <artist>", Category: "Entertainment"},
	}
}

func (p *ConcertsPlugin) Init() error { return nil }

func (p *ConcertsPlugin) OnReaction(_ ReactionContext) error { return nil }

func (p *ConcertsPlugin) OnMessage(ctx MessageContext) error {
	if !p.IsCommand(ctx.Body, "concerts") {
		return nil
	}

	args := p.GetArgs(ctx.Body, "concerts")
	if args == "" {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Usage: !concerts <artist> | !concerts watch|watching|unwatch <artist>")
	}

	parts := strings.SplitN(args, " ", 2)
	sub := strings.ToLower(parts[0])

	// DB-only subcommands
	switch sub {
	case "watching":
		return p.handleWatching(ctx)
	case "unwatch":
		if len(parts) < 2 || strings.TrimSpace(parts[1]) == "" {
			return p.SendReply(ctx.RoomID, ctx.EventID, "Usage: !concerts unwatch <artist>")
		}
		return p.handleUnwatch(ctx, strings.TrimSpace(parts[1]))
	}

	// API-calling subcommands run async
	safeGo("concerts-handler", func() {
		var err error
		switch sub {
		case "watch":
			if len(parts) < 2 || strings.TrimSpace(parts[1]) == "" {
				err = p.SendReply(ctx.RoomID, ctx.EventID, "Usage: !concerts watch <artist>")
			} else {
				err = p.handleWatch(ctx, strings.TrimSpace(parts[1]))
			}
		default:
			err = p.handleSearch(ctx, args)
		}
		if err != nil {
			slog.Error("concerts: handler error", "err", err)
		}
	})
	return nil
}

func (p *ConcertsPlugin) handleSearch(ctx MessageContext, artist string) error {
	if p.apiKey == "" {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Concert lookups are not configured (missing API key).")
	}

	// Daily rate limit (configurable, default 10/day to match TS version)
	concertLimit := 10
	if v := os.Getenv("RATELIMIT_CONCERTS"); v != "" {
		if n, err := fmt.Sscanf(v, "%d", &concertLimit); n != 1 || err != nil {
			concertLimit = 10
		}
	}
	if p.rateLimiter != nil && !p.rateLimiter.CheckLimit(ctx.Sender, "concerts", concertLimit) {
		return p.SendReply(ctx.RoomID, ctx.EventID, "Concert search rate limit reached for today.")
	}

	// Per-search cooldown
	cooldownKey := string(ctx.Sender) + ":" + strings.ToLower(artist)
	p.mu.Lock()
	if last, ok := p.cooldowns[cooldownKey]; ok && time.Since(last) < 30*time.Second {
		p.mu.Unlock()
		return p.SendReply(ctx.RoomID, ctx.EventID, "Please wait a moment before searching for this artist again.")
	}
	p.cooldowns[cooldownKey] = time.Now()
	p.mu.Unlock()

	events, err := p.fetchEvents(artist)
	if err != nil {
		slog.Error("concerts: fetch failed", "artist", artist, "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to fetch concert data.")
	}

	if len(events) == 0 {
		return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("No upcoming concerts found for %s.", artist))
	}

	// Show up to 5 events
	limit := 5
	if len(events) < limit {
		limit = len(events)
	}

	var sb strings.Builder
	sb.WriteString(fmt.Sprintf("Upcoming concerts for %s:\n\n", artist))

	for i := 0; i < limit; i++ {
		ev := events[i]
		dateStr := p.formatEventDate(ev.DateTime)
		location := p.formatLocation(ev.Venue.City, ev.Venue.Region, ev.Venue.Country)
		sb.WriteString(fmt.Sprintf("%d. %s\n", i+1, dateStr))
		sb.WriteString(fmt.Sprintf("   Venue: %s\n", ev.Venue.Name))
		sb.WriteString(fmt.Sprintf("   Location: %s\n", location))
		if ev.URL != "" {
			sb.WriteString(fmt.Sprintf("   Tickets: %s\n", ev.URL))
		}
		if i < limit-1 {
			sb.WriteString("\n")
		}
	}

	if len(events) > limit {
		sb.WriteString(fmt.Sprintf("\n...and %d more events.", len(events)-limit))
	}

	return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
}

func (p *ConcertsPlugin) fetchEvents(artist string) ([]bandsintownEvent, error) {
	d := db.Get()
	artistKey := strings.ToLower(artist)

	// Check cache (6h TTL)
	var cached string
	var cachedAt int64
	err := d.QueryRow(
		`SELECT data, cached_at FROM concerts_cache WHERE artist = ?`, artistKey,
	).Scan(&cached, &cachedAt)
	if err == nil && time.Now().Unix()-cachedAt < 6*3600 {
		var events []bandsintownEvent
		if json.Unmarshal([]byte(cached), &events) == nil {
			return events, nil
		}
	}

	// Fetch from API
	encoded := url.PathEscape(artist)
	apiURL := fmt.Sprintf("https://rest.bandsintown.com/artists/%s/events?app_id=%s&date=upcoming",
		encoded, p.apiKey)

	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, apiURL, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Accept", "application/json")

	resp, err := p.httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("bandsintown returned status %d", resp.StatusCode)
	}

	var events []bandsintownEvent
	if err := json.NewDecoder(resp.Body).Decode(&events); err != nil {
		return nil, err
	}

	// Update cache
	data, _ := json.Marshal(events)
	db.Exec("concerts: cache write",
		`INSERT INTO concerts_cache (artist, data, cached_at) VALUES (?, ?, ?)
		 ON CONFLICT(artist) DO UPDATE SET data = ?, cached_at = ?`,
		artistKey, string(data), time.Now().Unix(), string(data), time.Now().Unix(),
	)

	return events, nil
}

func (p *ConcertsPlugin) formatEventDate(dateStr string) string {
	layouts := []string{
		"2006-01-02T15:04:05",
		time.RFC3339,
		"2006-01-02",
	}
	for _, layout := range layouts {
		if t, err := time.Parse(layout, dateStr); err == nil {
			return t.Format("Mon, Jan 2, 2006 3:04 PM")
		}
	}
	return dateStr
}

func (p *ConcertsPlugin) formatLocation(city, region, country string) string {
	parts := []string{}
	if city != "" {
		parts = append(parts, city)
	}
	if region != "" {
		parts = append(parts, region)
	}
	if country != "" {
		parts = append(parts, country)
	}
	return strings.Join(parts, ", ")
}

func (p *ConcertsPlugin) handleWatch(ctx MessageContext, artist string) error {
	d := db.Get()
	artistKey := strings.ToLower(artist)
	_, err := d.Exec(
		`INSERT INTO concert_watchlist (user_id, artist, room_id) VALUES (?, ?, ?)
		 ON CONFLICT(user_id, artist) DO NOTHING`,
		string(ctx.Sender), artistKey, string(ctx.RoomID),
	)
	if err != nil {
		slog.Error("concerts: watch add", "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to add artist to watchlist.")
	}
	return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("Now watching %s for concert updates.", artist))
}

func (p *ConcertsPlugin) handleWatching(ctx MessageContext) error {
	d := db.Get()
	rows, err := d.Query(
		`SELECT artist FROM concert_watchlist WHERE user_id = ? ORDER BY artist`,
		string(ctx.Sender),
	)
	if err != nil {
		slog.Error("concerts: watching list", "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to load watchlist.")
	}
	defer rows.Close()

	var artists []string
	for rows.Next() {
		var a string
		if err := rows.Scan(&a); err != nil {
			continue
		}
		artists = append(artists, a)
	}

	if len(artists) == 0 {
		return p.SendReply(ctx.RoomID, ctx.EventID, "You're not watching any artists. Use !concerts watch <artist> to start.")
	}

	var sb strings.Builder
	sb.WriteString("Your Concert Watchlist:\n")
	for _, a := range artists {
		sb.WriteString(fmt.Sprintf("  - %s\n", a))
	}
	return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
}

func (p *ConcertsPlugin) handleUnwatch(ctx MessageContext, artist string) error {
	d := db.Get()
	artistKey := strings.ToLower(artist)
	res, err := d.Exec(
		`DELETE FROM concert_watchlist WHERE user_id = ? AND artist = ?`,
		string(ctx.Sender), artistKey,
	)
	if err != nil {
		slog.Error("concerts: unwatch", "err", err)
		return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to remove artist from watchlist.")
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("%s is not in your watchlist.", artist))
	}
	return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("Stopped watching %s.", artist))
}

// PostWeeklyDigest sends a weekly concert digest to a room and DMs watchers about upcoming shows.
// Intended to be called by the scheduler on Sundays.
func (p *ConcertsPlugin) PostWeeklyDigest(roomID id.RoomID) {
	if p.apiKey == "" {
		slog.Warn("concerts: skipping weekly digest, no API key configured")
		return
	}

	year, week := time.Now().UTC().ISOWeek()
	weekKey := fmt.Sprintf("%d-W%02d:%s", year, week, roomID)
	if db.JobCompleted("concert_digest", weekKey) {
		slog.Info("concerts: already sent digest this week", "week", weekKey)
		return
	}
	// Mark completed at the end only if we succeed (not deferred)
	success := false
	defer func() {
		if success {
			db.MarkJobCompleted("concert_digest", weekKey)
		}
	}()

	d := db.Get()

	// Get all unique watched artists
	rows, err := d.Query(`SELECT DISTINCT artist FROM concert_watchlist`)
	if err != nil {
		slog.Error("concerts: weekly digest query", "err", err)
		return
	}

	type watcherInfo struct {
		userID id.UserID
		roomID id.RoomID
	}

	artistWatchers := make(map[string][]watcherInfo)
	var allArtists []string

	// First collect all artists
	for rows.Next() {
		var artist string
		if err := rows.Scan(&artist); err != nil {
			continue
		}
		allArtists = append(allArtists, artist)
	}
	rows.Close()

	// Get watchers per artist
	for _, artist := range allArtists {
		wRows, err := d.Query(
			`SELECT user_id, room_id FROM concert_watchlist WHERE artist = ?`, artist,
		)
		if err != nil {
			continue
		}
		for wRows.Next() {
			var uid, rid string
			if err := wRows.Scan(&uid, &rid); err != nil {
				continue
			}
			artistWatchers[artist] = append(artistWatchers[artist], watcherInfo{
				userID: id.UserID(uid),
				roomID: id.RoomID(rid),
			})
		}
		wRows.Close()
	}

	// For each artist, fetch events and notify watchers
	for artist, watchers := range artistWatchers {
		events, err := p.fetchEvents(artist)
		if err != nil {
			slog.Error("concerts: weekly digest fetch", "artist", artist, "err", err)
			continue
		}

		if len(events) == 0 {
			continue
		}

		// Filter to events in the next 2 weeks
		now := time.Now()
		twoWeeks := now.Add(14 * 24 * time.Hour)
		var upcoming []bandsintownEvent
		for _, ev := range events {
			t, err := time.Parse("2006-01-02T15:04:05", ev.DateTime)
			if err != nil {
				t, err = time.Parse(time.RFC3339, ev.DateTime)
			}
			if err == nil && t.After(now) && t.Before(twoWeeks) {
				upcoming = append(upcoming, ev)
			}
		}

		if len(upcoming) == 0 {
			continue
		}

		// Build message
		var sb strings.Builder
		sb.WriteString(fmt.Sprintf("Upcoming shows for %s (next 2 weeks):\n\n", artist))
		for i, ev := range upcoming {
			if i >= 5 {
				sb.WriteString(fmt.Sprintf("\n...and %d more.", len(upcoming)-5))
				break
			}
			dateStr := p.formatEventDate(ev.DateTime)
			location := p.formatLocation(ev.Venue.City, ev.Venue.Region, ev.Venue.Country)
			sb.WriteString(fmt.Sprintf("- %s @ %s (%s)\n", dateStr, ev.Venue.Name, location))
		}

		msg := sb.String()
		for _, w := range watchers {
			if err := p.SendDM(w.userID, msg); err != nil {
				slog.Error("concerts: DM watcher", "user", w.userID, "err", err)
			}
		}
	}

	success = true
	slog.Info("concerts: weekly digest completed")
}
