package service

import (
	"context"
	"encoding/xml"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/TopherMayor/unified-media-manager/internal/cardigann"
	"golang.org/x/sync/errgroup"
)

type rssFeed struct {
	XMLName xml.Name   `xml:"rss"`
	Channel rssChannel `xml:"channel"`
}

type rssChannel struct {
	Items []rssItem `xml:"item"`
}

type rssItem struct {
	Title       string        `xml:"title"`
	GUID        string        `xml:"guid"`
	Link        string        `xml:"link"`
	PubDate     string        `xml:"pubDate"`
	Description string        `xml:"description"`
	Enclosure   *rssEnclosure `xml:"enclosure"`
	Attrs       []rssAttr     `xml:"attr"`
	Size        string        `xml:"size"`
}

type rssEnclosure struct {
	URL    string `xml:"url,attr"`
	Length string `xml:"length,attr"`
	Type   string `xml:"type,attr"`
}

type rssAttr struct {
	Name  string `xml:"name,attr"`
	Value string `xml:"value,attr"`
}

type SearchResult struct {
	Title           string        `json:"title"`
	GUID            string        `json:"guid"`
	Link            string        `json:"link"`
	Size            int64         `json:"size"`
	PubDate         string        `json:"pub_date"`
	IndexerName     string        `json:"indexer_name"`
	IndexerPriority int           `json:"indexer_priority"`
	Quality         ReleaseInfo   `json:"quality"`
	QualityTier     *QualityTier  `json:"quality_tier"`
	Seeders         int           `json:"seeders"`
	Peers           int           `json:"peers"`
	Category        string        `json:"category"`
	DownloadURL     string        `json:"download_url"`
	SourceIndexers  []string      `json:"source_indexers"`
}

type SearchRequest struct {
	Query      string `json:"query"`
	MediaType  string `json:"media_type"`
	IndexerIDs []int64 `json:"indexer_ids,omitempty"`
}

type SearchService struct {
	indexerSvc      *IndexerService
	parser          *ReleaseParser
	cardigannEngine *cardigann.CardigannEngine
	httpClient      *http.Client
}

func NewSearchService(indexerSvc *IndexerService, parser *ReleaseParser, cardigannEngine *cardigann.CardigannEngine) *SearchService {
	transport := &http.Transport{
		MaxIdleConns:        20,
		MaxIdleConnsPerHost: 5,
		IdleConnTimeout:     90 * time.Second,
		DisableKeepAlives:   false,
	}
	return &SearchService{
		indexerSvc:      indexerSvc,
		parser:          parser,
		cardigannEngine: cardigannEngine,
		httpClient:      &http.Client{Timeout: 30 * time.Second, Transport: transport},
	}
}

func (s *SearchService) Search(ctx context.Context, req SearchRequest) ([]SearchResult, error) {
	indexers, err := s.indexerSvc.ListEnabled(ctx)
	if err != nil {
		return nil, fmt.Errorf("get enabled indexers: %w", err)
	}

	if len(indexers) == 0 {
		return nil, fmt.Errorf("no enabled indexers available")
	}

	if len(req.IndexerIDs) > 0 {
		idSet := make(map[int64]bool, len(req.IndexerIDs))
		for _, id := range req.IndexerIDs {
			idSet[id] = true
		}
		var filtered []Indexer
		for _, idx := range indexers {
			if idSet[idx.ID] {
				filtered = append(filtered, idx)
			}
		}
		indexers = filtered
	}

	if len(indexers) == 0 {
		return nil, fmt.Errorf("no matching indexers found")
	}

	category := MediaTypeToCategory(req.MediaType)

	searchCtx, searchCancel := context.WithTimeout(ctx, 30*time.Second)
	defer searchCancel()

	var mu sync.Mutex
	var allResults []SearchResult

	g, gCtx := errgroup.WithContext(searchCtx)
	g.SetLimit(10)

	for _, idx := range indexers {
		idx := idx
		g.Go(func() error {
			results, err := s.searchIndexer(gCtx, idx, req.Query, category)
			if err != nil {
				return nil
			}

			mu.Lock()
			allResults = append(allResults, results...)
			mu.Unlock()

			return nil
		})
	}

	_ = g.Wait()

	merged := s.mergeAndDedup(allResults)

	sort.Slice(merged, func(i, j int) bool {
		ti := 0
		tj := 0
		if merged[i].QualityTier != nil {
			ti = merged[i].QualityTier.Rank
		}
		if merged[j].QualityTier != nil {
			tj = merged[j].QualityTier.Rank
		}
		if ti != tj {
			return ti > tj
		}
		return merged[i].Size < merged[j].Size
	})

	return merged, nil
}

func (s *SearchService) searchIndexer(ctx context.Context, idx Indexer, query string, category string) ([]SearchResult, error) {
	// Dispatch to Cardigann engine for cardigann implementation
	if idx.Implementation == "cardigann" {
		return s.searchCardigannIndexer(ctx, idx, query)
	}

	searchURL := fmt.Sprintf("%s/api?t=search&q=%s", idx.URL, url.QueryEscape(query))

	apiKey := ""
	if idx.APIKey != nil {
		apiKey = *idx.APIKey
	}
	if apiKey != "" {
		searchURL += "&apikey=" + apiKey
	}

	if category != "" {
		searchURL += "&cat=" + category
	}

	if idx.Implementation == "torznab" {
		searchURL += "&extended=1"
	}

	searchURL += "&offset=0&limit=100"

	reqCtx, reqCancel := context.WithTimeout(ctx, 15*time.Second)
	defer reqCancel()

	httpReq, err := http.NewRequestWithContext(reqCtx, http.MethodGet, searchURL, nil)
	if err != nil {
		go s.indexerSvc.RecordFailure(context.Background(), idx.ID)
		return nil, nil
	}

	client := s.httpClient
	resp, err := client.Do(httpReq)
	if err != nil {
		go s.indexerSvc.RecordFailure(context.Background(), idx.ID)
		return nil, nil
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 400 {
		go s.indexerSvc.RecordFailure(context.Background(), idx.ID)
		return nil, nil
	}

	body, err := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024))
	if err != nil {
		go s.indexerSvc.RecordFailure(context.Background(), idx.ID)
		return nil, nil
	}

	var feed rssFeed
	if err := xml.Unmarshal(body, &feed); err != nil {
		go s.indexerSvc.RecordFailure(context.Background(), idx.ID)
		slog.Error("failed to parse indexer XML response", "indexer", idx.Name, "error", err)
		return nil, nil
	}

	go s.indexerSvc.RecordSuccess(context.Background(), idx.ID)

	var results []SearchResult
	for _, item := range feed.Channel.Items {
		result := s.parseItem(item, idx)
		if result.DownloadURL != "" {
			results = append(results, result)
		}
	}

	return results, nil
}

func (s *SearchService) parseItem(item rssItem, idx Indexer) SearchResult {
	result := SearchResult{
		Title:           item.Title,
		GUID:            item.GUID,
		Link:            item.Link,
		PubDate:         item.PubDate,
		IndexerName:     idx.Name,
		IndexerPriority: idx.Priority,
		SourceIndexers:  []string{idx.Name},
	}

	if item.Enclosure != nil && item.Enclosure.URL != "" {
		result.DownloadURL = item.Enclosure.URL
		if item.Enclosure.Length != "" {
			if size, err := strconv.ParseInt(item.Enclosure.Length, 10, 64); err == nil {
				result.Size = size
			}
		}
	} else if item.Link != "" {
		result.DownloadURL = item.Link
	}

	if result.Size == 0 && item.Size != "" {
		if size, err := strconv.ParseInt(item.Size, 10, 64); err == nil {
			result.Size = size
		}
	}

	for _, attr := range item.Attrs {
		switch attr.Name {
		case "size":
			if result.Size == 0 {
				if size, err := strconv.ParseInt(attr.Value, 10, 64); err == nil {
					result.Size = size
				}
			}
		case "seeders":
			if v, err := strconv.Atoi(attr.Value); err == nil {
				result.Seeders = v
			}
		case "peers":
			if v, err := strconv.Atoi(attr.Value); err == nil {
				result.Peers = v
			}
		case "category":
			result.Category = attr.Value
		}
	}

	quality := s.parser.Parse(item.Title)
	result.Quality = quality
	result.QualityTier = s.parser.MatchQuality(quality)

	return result
}

func (s *SearchService) searchCardigannIndexer(ctx context.Context, idx Indexer, query string) ([]SearchResult, error) {
	cfg, err := s.indexerSvc.GetCardigannConfig(idx.Settings)
	if err != nil {
		slog.Error("failed to get cardigann config", "indexer", idx.Name, "error", err)
		go s.indexerSvc.RecordFailure(context.Background(), idx.ID)
		return nil, nil
	}

	def, err := cardigann.ParseDefinition([]byte(cfg.YAML))
	if err != nil {
		slog.Error("failed to parse cardigann YAML", "indexer", idx.Name, "error", err)
		go s.indexerSvc.RecordFailure(context.Background(), idx.ID)
		return nil, nil
	}

	results, err := s.cardigannEngine.Search(ctx, def, cfg.Config, cardigann.SearchQuery{
		Keywords: query,
	})
	if err != nil {
		slog.Error("cardigann search failed", "indexer", idx.Name, "error", err)
		go s.indexerSvc.RecordFailure(context.Background(), idx.ID)
		return nil, nil
	}

	go s.indexerSvc.RecordSuccess(context.Background(), idx.ID)

	var searchResults []SearchResult
	for _, cr := range results {
		result := SearchResult{
			Title:           cr.Title,
			GUID:            cr.GUID,
			DownloadURL:     cr.DownloadURL,
			Size:            cr.Size,
			PubDate:         cr.PubDate,
			IndexerName:     idx.Name,
			IndexerPriority: idx.Priority,
			SourceIndexers:  []string{idx.Name},
			Seeders:         cr.Seeders,
			Peers:           cr.Peers,
			Category:        cr.Category,
		}
		result.Quality = s.parser.Parse(cr.Title)
		result.QualityTier = s.parser.MatchQuality(result.Quality)
		searchResults = append(searchResults, result)
	}

	return searchResults, nil
}

func (s *SearchService) mergeAndDedup(results []SearchResult) []SearchResult {
	seen := make(map[string]int)
	var merged []SearchResult

	for _, r := range results {
		guid := strings.ToLower(r.GUID)
		if guid == "" {
			guid = strings.ToLower(r.DownloadURL)
		}
		if guid == "" {
			merged = append(merged, r)
			continue
		}

		if existingIdx, ok := seen[guid]; ok {
			existing := &merged[existingIdx]
			if r.IndexerPriority < existing.IndexerPriority {
				existing.SourceIndexers = append(existing.SourceIndexers, r.IndexerName)
			} else {
				newSources := make([]string, 0, len(existing.SourceIndexers)+1)
				newSources = append(newSources, r.IndexerName)
				newSources = append(newSources, existing.SourceIndexers...)
				r.SourceIndexers = newSources
				merged[existingIdx] = r
			}
		} else {
			seen[guid] = len(merged)
			merged = append(merged, r)
		}
	}

	return merged
}

type GrabRequest struct {
	DownloadURL string      `json:"download_url"`
	Title       string      `json:"title"`
	MediaType   string      `json:"media_type"`
	Quality     ReleaseInfo `json:"quality"`
	IndexerName string      `json:"indexer_name"`
	MediaID     int64       `json:"media_id"`
}

type GrabResult struct {
	QueueID    int64  `json:"queue_id"`
	DownloadID string `json:"download_id"`
	ClientName string `json:"client_name"`
	Protocol   string `json:"protocol"`
}

func (s *SearchService) Grab(ctx context.Context, req GrabRequest, downloadClientSvc *DownloadClientService) (*GrabResult, error) {
	protocol := "torrent"
	if strings.HasPrefix(req.DownloadURL, "magnet:?") {
		protocol = "torrent"
	} else if strings.HasSuffix(strings.ToLower(req.DownloadURL), ".nzb") {
		protocol = "nzb"
	}

	client, cfg, err := downloadClientSvc.GetClient(ctx, protocol)
	if err != nil {
		return nil, fmt.Errorf("get download client: %w", err)
	}

	downloadID, err := client.Add(ctx, req.DownloadURL, "umm")
	if err != nil {
		return nil, fmt.Errorf("add download: %w", err)
	}

	return &GrabResult{
		DownloadID: downloadID,
		ClientName: cfg.Name,
		Protocol:   cfg.Protocol,
	}, nil
}
