package controllers

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	neturl "net/url"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"sync"
	"time"

	"fotbal-club/internal/models"

	"github.com/PuerkitoBio/goquery"
	"github.com/gin-gonic/gin"
	"gorm.io/gorm"
)

// In-memory cache for logo lookup
var logoCache = map[string]string{}

// club data cache (JSON bytes) with TTL and disk persistence
type cachedItem struct {
	Data     []byte    `json:"data"`
	StoredAt time.Time `json:"stored_at"`
}

var (
	clubCacheMu sync.RWMutex
	clubCache   = map[string]cachedItem{}
	cacheTTL    = 30 * time.Minute
)

func cacheDir() string {
	return filepath.Join("cache", "facr")
}

func cachePath(key string) string {
	return filepath.Join(cacheDir(), key+".json")
}

func getCachedJSON(key string) ([]byte, bool) {
	// memory first
	clubCacheMu.RLock()
	item, ok := clubCache[key]
	clubCacheMu.RUnlock()
	if ok && time.Since(item.StoredAt) < cacheTTL {
		return item.Data, true
	}
	// disk fallback
	b, err := os.ReadFile(cachePath(key))
	if err == nil {
		var disk cachedItem
		if json.Unmarshal(b, &disk) == nil {
			if time.Since(disk.StoredAt) < cacheTTL {
				// warm memory
				clubCacheMu.Lock()
				clubCache[key] = disk
				clubCacheMu.Unlock()
				return disk.Data, true
			}
		}
	}
	return nil, false
}

func setCachedJSON(key string, data []byte) {
	item := cachedItem{Data: data, StoredAt: time.Now()}
	clubCacheMu.Lock()
	clubCache[key] = item
	clubCacheMu.Unlock()
	// persist to disk (best-effort)
	_ = os.MkdirAll(cacheDir(), 0o755)
	if b, err := json.Marshal(item); err == nil {
		_ = os.WriteFile(cachePath(key), b, 0o644)
	}
}

// ----- Types (mirroring facr-scraper) -----

type Competition struct {
	ID          string            `json:"id"`
	Code        string            `json:"code"`
	Name        string            `json:"name"`
	TeamCount   string            `json:"team_count"`
	MatchesLink string            `json:"matches_link"`
	Matches     []Match           `json:"matches,omitempty"`
	Table       *CompetitionTable `json:"table,omitempty"`
}

type CompetitionTable struct {
	Overall []TableRow `json:"overall"`
}

type ClubInfo struct {
	Name           string        `json:"name"`
	ClubID         string        `json:"club_id"`
	ClubType       string        `json:"club_type"`
	ClubInternalID string        `json:"club_internal_id,omitempty"`
	URL            string        `json:"url,omitempty"`
	LogoURL        string        `json:"logo_url,omitempty"`
	Address        string        `json:"address,omitempty"`
	Category       string        `json:"category,omitempty"`
	Competitions   []Competition `json:"competitions"`
}

type SearchResult struct {
	Name     string `json:"name"`
	ClubID   string `json:"club_id"`
	ClubType string `json:"club_type"`
	URL      string `json:"url"`
	LogoURL  string `json:"logo_url"`
	Category string `json:"category,omitempty"`
	Address  string `json:"address,omitempty"`
}

type Match struct {
	DateTime      string `json:"date_time"`
	Home          string `json:"home"`
	HomeID        string `json:"home_id,omitempty"`
	HomeLogoURL   string `json:"home_logo_url,omitempty"`
	Away          string `json:"away"`
	AwayID        string `json:"away_id,omitempty"`
	AwayLogoURL   string `json:"away_logo_url,omitempty"`
	Score         string `json:"score"`
	Venue         string `json:"venue"`
	Note          string `json:"note,omitempty"`
	MatchID       string `json:"match_id"`
	ReportURL     string `json:"report_url,omitempty"`
	DelegationURL string `json:"delegation_url,omitempty"`
}

type TableRow struct {
	Rank        string `json:"rank"`
	Team        string `json:"team"`
	TeamID      string `json:"team_id,omitempty"`
	TeamLogoURL string `json:"team_logo_url,omitempty"`
	Played      string `json:"played"`
	Wins        string `json:"wins"`
	Draws       string `json:"draws"`
	Losses      string `json:"losses"`
	Score       string `json:"score"`
	Points      string `json:"points"`
}

// ----- Helpers -----

func containsFold(s, substr string) bool {
	s = strings.ToLower(strings.TrimSpace(s))
	substr = strings.ToLower(strings.TrimSpace(substr))
	if substr == "" {
		return false
	}
	return strings.Contains(s, substr)
}

func extractUUIDFromHref(href string) string {
	href = strings.TrimSpace(href)
	if href == "" {
		return ""
	}
	re := regexp.MustCompile(`[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`)
	if m := re.FindString(href); m != "" {
		return m
	}
	parts := strings.Split(href, "/")
	if len(parts) > 0 {
		cand := parts[len(parts)-1]
		if re.MatchString(cand) {
			return cand
		}
	}
	return ""
}

func getLogoBySearch(name string) string {
	key := strings.ToLower(strings.TrimSpace(name))
	if key == "" {
		return ""
	}
	if v, ok := logoCache[key]; ok {
		return v
	}
	// Query local API routed through this same server
	apiURL := fmt.Sprintf("http://localhost:8080/api/v1/facr/club/search?q=%s", neturl.QueryEscape(name))
	client := &http.Client{Timeout: 5 * time.Second}
	resp, err := client.Get(apiURL)
	if err != nil {
		return ""
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		io.Copy(io.Discard, resp.Body)
		return ""
	}
	var payload struct {
		Results []struct {
			Name    string `json:"name"`
			LogoURL string `json:"logo_url"`
		} `json:"results"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
		return ""
	}
	best := ""
	for _, r := range payload.Results {
		if strings.EqualFold(strings.TrimSpace(r.Name), strings.TrimSpace(name)) {
			best = r.LogoURL
			break
		}
	}
	if best == "" {
		for _, r := range payload.Results {
			if strings.Contains(strings.ToLower(r.Name), key) || strings.Contains(key, strings.ToLower(r.Name)) {
				best = r.LogoURL
				break
			}
		}
	}
	if best == "" && len(payload.Results) > 0 {
		best = payload.Results[0].LogoURL
	}
	if best != "" {
		logoCache[key] = best
		return best
	}

	// Fallback: directly scrape fotbal.cz search (same logic as SearchClubs)
	vals := neturl.Values{}
	vals.Set("q", name)
	searchURL := "https://www.fotbal.cz/club/hledej?" + vals.Encode()
	req, err := http.NewRequest("GET", searchURL, nil)
	if err != nil {
		return ""
	}
	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36")
	req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
	req.Header.Set("Accept-Language", "cs-CZ,cs;q=0.9,en;q=0.8")
	req.Header.Set("Referer", "https://www.fotbal.cz/club/hledej")
	resp2, err := client.Do(req)
	if err != nil {
		return ""
	}
	defer resp2.Body.Close()
	if resp2.StatusCode != http.StatusOK {
		io.Copy(io.Discard, resp2.Body)
		return ""
	}
	doc, err := goquery.NewDocumentFromReader(resp2.Body)
	if err != nil {
		return ""
	}
	// choose first exact match, else first contains, else empty
	exact := ""
	partial := ""
	doc.Find("li.ListItemSplit").Each(func(_ int, li *goquery.Selection) {
		a := li.Find("a.Link--inverted").First()
		n := strings.TrimSpace(a.Find("span.H7").First().Text())
		if n == "" {
			n = strings.TrimSpace(a.Text())
		}
		img := a.Find("img").First()
		src, _ := img.Attr("src")
		if src == "" {
			return
		}
		if exact == "" && strings.EqualFold(n, name) {
			exact = src
		}
		if partial == "" && (strings.Contains(strings.ToLower(n), key) || strings.Contains(key, strings.ToLower(n))) {
			partial = src
		}
	})
	best = exact
	if best == "" {
		best = partial
	}
	if best != "" {
		logoCache[key] = best
	}
	return best
}

func getLogo(teamName, teamID string) string {
	placeholder := "/dist/img/logo-club-empty.svg"
	name := strings.ToLower(strings.TrimSpace(teamName))
	if name == "" || strings.Contains(name, "volno") || strings.Contains(name, "volný los") || strings.Contains(name, "volny los") || strings.Contains(name, "bye") {
		return placeholder
	}
	if logo := getLogoBySearch(teamName); logo != "" {
		return logo
	}
	tid := strings.TrimSpace(teamID)
	if tid != "" {
		return fmt.Sprintf("https://is1.fotbal.cz/media/kluby/%s/%s_crop.jpg", tid, tid)
	}
	return placeholder
}

func resolveISURL(href string) string {
	href = strings.TrimSpace(href)
	if strings.HasPrefix(href, "http://") || strings.HasPrefix(href, "https://") {
		if u, err := neturl.Parse(href); err == nil {
			u.Scheme = "https"
			u.Host = "is.fotbal.cz"
			if !strings.HasPrefix(u.Path, "/public/") {
				if strings.HasPrefix(u.Path, "/zapasy/") {
					u.Path = "/public" + u.Path
				}
			}
			q := u.Query()
			q.Del("discipline")
			u.RawQuery = q.Encode()
			return u.String()
		}
		return href
	}
	href = strings.TrimPrefix(href, "./")
	for strings.HasPrefix(href, "../") {
		href = strings.TrimPrefix(href, "../")
	}
	href = strings.TrimPrefix(href, "/")
	path := "/public/" + href
	u := neturl.URL{Scheme: "https", Host: "is.fotbal.cz", Path: path}
	return u.String()
}

// ----- Controller -----

type FACRController struct{ DB *gorm.DB }

func NewFACRController(db *gorm.DB) *FACRController { return &FACRController{DB: db} }

// GET /api/v1/facr/club/search?q=
func (fc *FACRController) SearchClubs(c *gin.Context) {
	q := strings.TrimSpace(c.Query("q"))
	if q == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter 'q' is required"})
		return
	}
	vals := neturl.Values{}
	vals.Set("q", q)
	searchURL := "https://www.fotbal.cz/club/hledej?" + vals.Encode()

	req, err := http.NewRequest("GET", searchURL, nil)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Error creating request: %v", err)})
		return
	}
	req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36")
	req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
	req.Header.Set("Accept-Language", "cs-CZ,cs;q=0.9,en;q=0.8")
	req.Header.Set("Referer", "https://www.fotbal.cz/club/hledej")
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Error fetching search page: %v", err)})
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		// Try once more with quoted query if short tokens present
		resp.Body.Close()
		searchURL2 := searchURL
		tokens := strings.Fields(q)
		for _, t := range tokens {
			if len([]rune(t)) <= 2 {
				vals2 := neturl.Values{}
				vals2.Set("q", "\""+q+"\"")
				searchURL2 = "https://www.fotbal.cz/club/hledej?" + vals2.Encode()
				break
			}
		}
		req2, _ := http.NewRequest("GET", searchURL2, nil)
		req2.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36")
		req2.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
		req2.Header.Set("Accept-Language", "en-US,en;q=0.9")
		resp2, err2 := client.Do(req2)
		if err2 != nil {
			c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Error fetching (retry): %v", err2)})
			return
		}
		defer resp2.Body.Close()
		if resp2.StatusCode != http.StatusOK {
			c.JSON(http.StatusOK, gin.H{"query": q, "count": 0, "results": []SearchResult{}})
			return
		}
		resp = resp2
	}

	doc, err := goquery.NewDocumentFromReader(resp.Body)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Error parsing HTML: %v", err)})
		return
	}
	var results []SearchResult
	doc.Find("li.ListItemSplit").Each(func(_ int, li *goquery.Selection) {
		a := li.Find("a.Link--inverted").First()
		href, _ := a.Attr("href")
		if href == "" {
			return
		}
		name := strings.TrimSpace(a.Find("span.H7").First().Text())
		if name == "" {
			name = strings.TrimSpace(a.Text())
		}
		img := a.Find("img").First()
		logoURL, _ := img.Attr("src")
		category := strings.TrimSpace(li.Find(".ClubCategories .BadgeCategory").First().Text())
		address := strings.TrimSpace(li.Find(".ClubAddress p").First().Text())
		clubType := "football"
		if strings.Contains(strings.ToLower(href), "/futsal/") {
			clubType = "futsal"
		}
		parts := strings.Split(strings.TrimRight(href, "/"), "/")
		clubID := ""
		if len(parts) > 0 {
			clubID = parts[len(parts)-1]
		}
		if !strings.HasPrefix(href, "http://") && !strings.HasPrefix(href, "https://") {
			href = "https://www.fotbal.cz" + href
		}
		results = append(results, SearchResult{
			Name:     name,
			ClubID:   clubID,
			ClubType: clubType,
			URL:      href,
			LogoURL:  logoURL,
			Category: category,
			Address:  address,
		})
	})

	// If setup is completed and a sport is configured, filter results to that sport only
	if fc.DB != nil {
		var s models.Settings
		if err := fc.DB.First(&s).Error; err == nil {
			if strings.TrimSpace(s.ClubID) != "" && strings.TrimSpace(s.ClubType) != "" {
				filtered := make([]SearchResult, 0, len(results))
				want := strings.ToLower(strings.TrimSpace(s.ClubType))
				for _, r := range results {
					if strings.ToLower(strings.TrimSpace(r.ClubType)) == want {
						filtered = append(filtered, r)
					}
				}
				results = filtered
			}
		}
	}

	// respond and close the function
	c.JSON(http.StatusOK, gin.H{
		"query":   q,
		"count":   len(results),
		"results": results,
	})
}

// GET /api/v1/facr/club/:type/:id
func (fc *FACRController) GetClubInfo(c *gin.Context) {
	clubID := c.Param("id")
	clubType := c.Param("type")
	if clubID == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Club ID is required"})
		return
	}
	cacheKey := fmt.Sprintf("%s_%s_info", clubType, clubID)
	force := c.Query("force") == "1"
	if !force {
		if data, ok := getCachedJSON(cacheKey); ok {
			c.Data(http.StatusOK, "application/json", data)
			return
		}
	}
	external := fmt.Sprintf("https://facr.tdvorak.dev/club/%s/%s", clubType, clubID)
	httpClient := &http.Client{Timeout: 15 * time.Second}
	resp, err := httpClient.Get(external)
	if err != nil {
		c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("proxy error: %v", err)})
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		c.Data(resp.StatusCode, "application/json", body)
		return
	}
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("read error: %v", err)})
		return
	}
	setCachedJSON(cacheKey, b)
	c.Data(http.StatusOK, "application/json", b)
}

// GET /api/v1/facr/club/:type/:id/table
func (fc *FACRController) GetClubTables(c *gin.Context) {
	clubID := c.Param("id")
	clubType := c.Param("type")
	if clubID == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Club ID is required"})
		return
	}
	cacheKey := fmt.Sprintf("%s_%s_table", clubType, clubID)
	force := c.Query("force") == "1"
	if !force {
		if data, ok := getCachedJSON(cacheKey); ok {
			c.Data(http.StatusOK, "application/json", data)
			return
		}
	}
	external := fmt.Sprintf("https://facr.tdvorak.dev/club/%s/%s/table", clubType, clubID)
	httpClient := &http.Client{Timeout: 15 * time.Second}
	resp, err := httpClient.Get(external)
	if err != nil {
		c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("proxy error: %v", err)})
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		c.Data(resp.StatusCode, "application/json", body)
		return
	}
	b, err := io.ReadAll(resp.Body)
	if err != nil {
		c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("read error: %v", err)})
		return
	}
	setCachedJSON(cacheKey, b)
	c.Data(http.StatusOK, "application/json", b)
}
