package service

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/TopherMayor/unified-media-manager/internal/db"
)

type MetadataSearchResult struct {
	ProviderID    string          `json:"provider_id"`
	Title         string          `json:"title"`
	Year          *int            `json:"year,omitempty"`
	MediaType     string          `json:"media_type"`
	Overview      string          `json:"overview,omitempty"`
	OriginalTitle string          `json:"original_title,omitempty"`
	ExternalIDs   json.RawMessage `json:"external_ids,omitempty"`
}

type MetadataDetails struct {
	ProviderID    string          `json:"provider_id"`
	Title         string          `json:"title"`
	OriginalTitle *string         `json:"original_title,omitempty"`
	Overview      *string         `json:"overview,omitempty"`
	Year          *int            `json:"year,omitempty"`
	Ratings       json.RawMessage `json:"ratings,omitempty"`
	ExternalIDs   json.RawMessage `json:"external_ids,omitempty"`
	Metadata      json.RawMessage `json:"metadata,omitempty"`
	Images        []ImageResult   `json:"images,omitempty"`
}

type ImageResult struct {
	URL    string `json:"url"`
	Type   string `json:"type"`
	Width  int    `json:"width,omitempty"`
	Height int    `json:"height,omitempty"`
}

type SearchOptions struct {
	Year      *int
	MediaType string
	Page      int
}

type MetadataProvider interface {
	Name() string
	Search(ctx context.Context, query string, opts SearchOptions) ([]MetadataSearchResult, error)
	GetDetails(ctx context.Context, id string) (*MetadataDetails, error)
	GetImages(ctx context.Context, id string) ([]ImageResult, error)
}

type MetadataService struct {
	db         *db.DB
	mediaSvc   *MediaService
	providers  map[string]MetadataProvider
	imageDir   string
	httpClient *http.Client
}

func NewMetadataService(database *db.DB, mediaSvc *MediaService, imageDir string) *MetadataService {
	return &MetadataService{
		db:        database,
		mediaSvc:  mediaSvc,
		providers: make(map[string]MetadataProvider),
		imageDir:  imageDir,
		httpClient: &http.Client{
			Timeout: 10 * time.Second,
		},
	}
}

func (s *MetadataService) RegisterProvider(provider MetadataProvider) {
	s.providers[provider.Name()] = provider
}

func (s *MetadataService) GetCached(ctx context.Context, provider, providerID string) (*MetadataDetails, error) {
	var data []byte
	err := s.db.Pool.QueryRow(ctx,
		"SELECT data FROM metadata_cache WHERE provider = $1 AND provider_id = $2 AND expires_at > NOW()",
		provider, providerID).Scan(&data)
	if err != nil {
		return nil, nil
	}

	var details MetadataDetails
	if err := json.Unmarshal(data, &details); err != nil {
		return nil, nil
	}
	return &details, nil
}

func (s *MetadataService) SetCached(ctx context.Context, provider, providerID, mediaType string, data *MetadataDetails, ttl time.Duration) error {
	jsonData, err := json.Marshal(data)
	if err != nil {
		return fmt.Errorf("marshal cache data: %w", err)
	}

	_, err = s.db.Pool.Exec(ctx,
		`INSERT INTO metadata_cache (provider, provider_id, media_type, data, expires_at)
		 VALUES ($1, $2, $3, $4, NOW() + $5::interval)
		 ON CONFLICT (provider, provider_id) DO UPDATE SET data = $4, media_type = $3, cached_at = NOW(), expires_at = NOW() + $5::interval`,
		provider, providerID, mediaType, jsonData, fmt.Sprintf("%d seconds", int(ttl.Seconds())))
	if err != nil {
		return fmt.Errorf("upsert metadata cache: %w", err)
	}
	return nil
}

func (s *MetadataService) DownloadImage(ctx context.Context, imageURL, mediaType, filename string) error {
	dir := filepath.Join(s.imageDir, mediaType)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return fmt.Errorf("create image directory: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, imageURL, nil)
	if err != nil {
		return fmt.Errorf("create image request: %w", err)
	}

	resp, err := s.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("download image: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("download image failed: status %d", resp.StatusCode)
	}

	contentType := resp.Header.Get("Content-Type")
	if !strings.HasPrefix(contentType, "image/") {
		return fmt.Errorf("invalid content type: %s", contentType)
	}

	destPath := filepath.Join(dir, filename)
	f, err := os.Create(destPath)
	if err != nil {
		return fmt.Errorf("create image file: %w", err)
	}
	defer f.Close()

	if _, err := io.Copy(f, resp.Body); err != nil {
		return fmt.Errorf("write image file: %w", err)
	}

	return nil
}

func (s *MetadataService) RefreshMetadata(ctx context.Context, mediaID int64, mediaType string) error {
	detail, err := s.mediaSvc.GetByID(ctx, mediaID, mediaType)
	if err != nil {
		return fmt.Errorf("get media: %w", err)
	}

	media := detail.Media

	var existingIDs map[string]interface{}
	if err := json.Unmarshal(media.ExternalIDs, &existingIDs); err != nil {
		existingIDs = make(map[string]interface{})
	}

	var allMetadata map[string]interface{}
	if err := json.Unmarshal(media.Metadata, &allMetadata); err != nil {
		allMetadata = make(map[string]interface{})
	}

	var existingImages []map[string]interface{}
	if err := json.Unmarshal(media.Images, &existingImages); err != nil {
		existingImages = []map[string]interface{}{}
	}

	var updatedOverview *string
	var updatedOriginalTitle *string
	var updatedYear *int

	for name, provider := range s.providers {
		providerID := ""
		if idVal, ok := existingIDs[name]; ok {
			providerID = fmt.Sprintf("%v", idVal)
		}

		if providerID == "" {
			results, err := provider.Search(ctx, media.Title, SearchOptions{
				Year:      media.Year,
				MediaType: mediaType,
			})
			if err != nil {
				slog.Error("provider search failed", "provider", name, "error", err)
				continue
			}
			if len(results) == 0 {
				continue
			}
			providerID = results[0].ProviderID		}

		cached, _ := s.GetCached(ctx, name, providerID)
		var metaDetails *MetadataDetails
		if cached != nil {
			metaDetails = cached
		} else {
			details, err := provider.GetDetails(ctx, providerID)
			if err != nil {
				slog.Error("provider get details failed", "provider", name, "error", err)
				continue
			}
			metaDetails = details

			if err := s.SetCached(ctx, name, providerID, mediaType, details, 7*24*time.Hour); err != nil {
				slog.Error("cache metadata failed", "provider", name, "error", err)
			}
		}

		existingIDs[name] = providerID
		if metaDetails.ExternalIDs != nil {
			var providerIDs map[string]interface{}
			if err := json.Unmarshal(metaDetails.ExternalIDs, &providerIDs); err == nil {
				for k, v := range providerIDs {
					existingIDs[k] = v
				}
			}
		}

		if metaDetails.Overview != nil && *metaDetails.Overview != "" {
			updatedOverview = metaDetails.Overview
		}
		if metaDetails.OriginalTitle != nil && *metaDetails.OriginalTitle != "" {
			updatedOriginalTitle = metaDetails.OriginalTitle
		}
		if metaDetails.Year != nil {
			updatedYear = metaDetails.Year
		}

		if metaDetails.Metadata != nil {
			var providerMeta map[string]interface{}
			if err := json.Unmarshal(metaDetails.Metadata, &providerMeta); err == nil {
				allMetadata[name] = providerMeta
			}
		}
		if metaDetails.Ratings != nil {
			var ratings map[string]interface{}
			if err := json.Unmarshal(metaDetails.Ratings, &ratings); err == nil {
				allMetadata[name+"_ratings"] = ratings
			}
		}

		images, err := provider.GetImages(ctx, providerID)
		if err != nil {
			slog.Error("provider get images failed", "provider", name, "error", err)
			continue
		}

		for _, img := range images {
			ext := filepath.Ext(img.URL)
			if ext == "" {
				ext = ".jpg"
			}
			filename := fmt.Sprintf("%s_%s_%d%s", name, img.Type, mediaID, ext)
			imgCtx, imgCancel := context.WithTimeout(ctx, 15*time.Second)
			if err := s.DownloadImage(imgCtx, img.URL, mediaType, filename); err != nil {
				slog.Error("download image failed", "provider", name, "error", err)
				imgCancel()
				continue
			}
			imgCancel()

			localPath := fmt.Sprintf("/api/images/%s/%s", mediaType, filename)
			existingImages = append(existingImages, map[string]interface{}{
				"url":     localPath,
				"type":    img.Type,
				"width":   img.Width,
				"height":  img.Height,
				"source":  name,
			})
		}
	}

	externalIDsJSON, _ := json.Marshal(existingIDs)
	metadataJSON, _ := json.Marshal(allMetadata)
	imagesJSON, _ := json.Marshal(existingImages)

	updateReq := UpdateMediaRequest{
		ExternalIDs: externalIDsJSON,
		Metadata:    metadataJSON,
		Images:      imagesJSON,
	}
	if updatedOverview != nil {
		updateReq.Overview = updatedOverview
	}
	if updatedOriginalTitle != nil {
		updateReq.OriginalTitle = updatedOriginalTitle
	}
	if updatedYear != nil {
		updateReq.Year = updatedYear
	}

	if err := s.mediaSvc.Update(ctx, mediaID, mediaType, updateReq); err != nil {
		return fmt.Errorf("update media metadata: %w", err)
	}

	return nil
}

func (s *MetadataService) RefreshAllMetadata(ctx context.Context) error {
	rows, err := s.db.Pool.Query(ctx,
		"SELECT id, media_type FROM media WHERE monitored = true AND deleted_at IS NULL")
	if err != nil {
		return fmt.Errorf("query monitored media: %w", err)
	}
	defer rows.Close()

	for rows.Next() {
		var id int64
		var mediaType string
		if err := rows.Scan(&id, &mediaType); err != nil {
			slog.Error("scan media row", "error", err)
			continue
		}

		itemCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
		if err := s.RefreshMetadata(itemCtx, id, mediaType); err != nil {
			slog.Error("refresh metadata failed", "media_id", id, "error", err)
		}
		cancel()
	}

	return nil
}
