package service

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"log/slog"
	"path/filepath"
	"strings"
	"time"

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

// FullMediaDetail is the comprehensive detail response for the MediaDetail page.
type FullMediaDetail struct {
	Media          MediaDetail         `json:"media"`
	QualityProfile *QualityProfileInfo `json:"quality_profile,omitempty"`
	FilesWithSubs  []FileWithSubtitles `json:"files_with_subtitles"`
	Episodes       []EpisodeInfo       `json:"episodes,omitempty"`
	History        []MediaHistoryItem  `json:"history"`
}

// QualityProfileInfo contains the quality profile data for the detail response.
type QualityProfileInfo struct {
	ID               int64           `json:"id"`
	Name             string          `json:"name"`
	CutoffQuality    json.RawMessage `json:"cutoff_quality"`
	AllowedQualities json.RawMessage `json:"allowed_qualities"`
}

// FileWithSubtitles extends MediaFile with associated subtitle information.
type FileWithSubtitles struct {
	MediaFile
	Subtitles []SubtitleInfo `json:"subtitles,omitempty"`
}

// SubtitleInfo represents a subtitle file associated with a media file.
type SubtitleInfo struct {
	FileName     string `json:"file_name"`
	Language     string `json:"language"`
	LanguageCode string `json:"language_code"`
	HI           bool   `json:"hi"`
	Forced       bool   `json:"forced"`
	Source       string `json:"source"`
}

// EpisodeInfo represents a single episode within a series.
type EpisodeInfo struct {
	MediaID   int64           `json:"media_id"`
	Title     string          `json:"title"`
	Season    int             `json:"season"`
	Episode   int             `json:"episode"`
	Status    string          `json:"status"`
	Monitored bool            `json:"monitored"`
	AirDate   *string         `json:"air_date,omitempty"`
	HasFile   bool            `json:"has_file"`
	Quality   json.RawMessage `json:"quality,omitempty"`
}

// MediaHistoryItem represents an activity event in the media detail history.
type MediaHistoryItem struct {
	ID          int64           `json:"id"`
	EventType   string          `json:"event_type"`
	Title       string          `json:"title"`
	Description *string         `json:"description,omitempty"`
	Data        json.RawMessage `json:"data"`
	CreatedAt   time.Time       `json:"created_at"`
}

// MediaDetailService aggregates data for the media detail page using pgx.Batch.
type MediaDetailService struct {
	db          *db.DB
	mediaSvc    *MediaService
	activitySvc *ActivityService
}

// NewMediaDetailService creates a new MediaDetailService.
func NewMediaDetailService(database *db.DB, mediaSvc *MediaService, activitySvc *ActivityService) *MediaDetailService {
	return &MediaDetailService{db: database, mediaSvc: mediaSvc, activitySvc: activitySvc}
}

// GetFullDetail returns the complete media detail using pgx.Batch for parallel queries.
func (s *MediaDetailService) GetFullDetail(ctx context.Context, id int64, mediaType string) (*FullMediaDetail, error) {
	// Step 1: Get base media detail via existing service
	baseDetail, err := s.mediaSvc.GetByID(ctx, id, mediaType)
	if err != nil {
		return nil, fmt.Errorf("get media detail: %w", err)
	}

	result := &FullMediaDetail{
		Media: *baseDetail,
	}

	// Step 2: Build a pgx.Batch with queries
	batch := &pgx.Batch{}

	hasQualityProfile := baseDetail.Media.QualityProfileID != nil
	hasEpisodes := mediaType == "series"

	// Query: Quality profile
	if hasQualityProfile {
		batch.Queue(
			"SELECT id, name, cutoff_quality, allowed_qualities FROM quality_profiles WHERE id = $1",
			*baseDetail.Media.QualityProfileID,
		)
	}

	// Query: Activity history
	batch.Queue(
		"SELECT id, event_type, title, description, data, created_at FROM activity_events WHERE media_id = $1 ORDER BY created_at DESC LIMIT 100",
		id,
	)

	// Query: Episode children (series only)
	if hasEpisodes {
		batch.Queue(
			`SELECT m.id, m.title, mr.season, mr.position, m.status, m.monitored,
				EXISTS(SELECT 1 FROM media_files mf WHERE mf.media_id = m.id AND mf.deleted_at IS NULL) as has_file,
				mf.quality
				FROM media m
				JOIN media_relations mr ON mr.child_id = m.id
				LEFT JOIN LATERAL (SELECT quality FROM media_files WHERE media_id = m.id AND deleted_at IS NULL ORDER BY created_at DESC LIMIT 1) mf ON true
				WHERE mr.parent_id = $1 AND mr.relation = 'episode'
				ORDER BY mr.season, mr.position`,
			id,
		)
	}

	// Step 3: Send batch
	batchResults := s.db.Pool.SendBatch(ctx, batch)
	defer batchResults.Close()

	// Step 4: Read results in the same order they were queued

	// Read quality profile
	if hasQualityProfile {
		row := batchResults.QueryRow()
		var qp QualityProfileInfo
		if err := row.Scan(&qp.ID, &qp.Name, &qp.CutoffQuality, &qp.AllowedQualities); err == nil {
			result.QualityProfile = &qp
		}
	}

	// Read activity history
	historyRows, err := batchResults.Query()
	if err != nil {
		slog.Error("read activity history from batch", "error", err)
	} else {
		defer historyRows.Close()
		for historyRows.Next() {
			var item MediaHistoryItem
			var description sql.NullString
			var data []byte
			if err := historyRows.Scan(&item.ID, &item.EventType, &item.Title, &description, &data, &item.CreatedAt); err != nil {
				slog.Error("scan history item", "error", err)
				continue
			}
			if description.Valid {
				item.Description = &description.String
			}
			if data != nil {
				item.Data = json.RawMessage(data)
			}
			result.History = append(result.History, item)
		}
	}

	// Read episodes (series only)
	if hasEpisodes {
		epRows, err := batchResults.Query()
		if err != nil {
			slog.Error("read episodes from batch", "error", err)
		} else {
			defer epRows.Close()
			for epRows.Next() {
				var ep EpisodeInfo
				var season, position sql.NullInt64
				var airDate sql.NullString
				var quality []byte
				if err := epRows.Scan(&ep.MediaID, &ep.Title, &season, &position, &ep.Status, &ep.Monitored, &ep.HasFile, &quality); err != nil {
					slog.Error("scan episode", "error", err)
					continue
				}
				if season.Valid {
					ep.Season = int(season.Int64)
				}
				if position.Valid {
					ep.Episode = int(position.Int64)
				}
				if airDate.Valid {
					ep.AirDate = &airDate.String
				}
				if quality != nil {
					ep.Quality = json.RawMessage(quality)
				}
				result.Episodes = append(result.Episodes, ep)
			}
		}
	}

	// Step 5: Build subtitle info for each media file (from DB cache)
	result.FilesWithSubs = s.buildFilesWithSubtitlesFromDB(ctx, baseDetail.Files)

	return result, nil
}

func (s *MediaDetailService) buildFilesWithSubtitlesFromDB(ctx context.Context, files []MediaFile) []FileWithSubtitles {
	if len(files) == 0 {
		return []FileWithSubtitles{}
	}

	fileIDs := make([]interface{}, len(files))
	for i, f := range files {
		fileIDs[i] = f.ID
	}

	subMap := make(map[int64][]SubtitleInfo)
	if len(fileIDs) > 0 {
		rows, err := s.db.Pool.Query(ctx,
			`SELECT media_file_id, file_name, language, language_code, hi, forced, source
			 FROM media_subtitles WHERE media_file_id = ANY($1)`, fileIDs)
		if err == nil {
			defer rows.Close()
			for rows.Next() {
				var fileID int64
				var sub SubtitleInfo
				if err := rows.Scan(&fileID, &sub.FileName, &sub.Language, &sub.LanguageCode, &sub.HI, &sub.Forced, &sub.Source); err == nil {
					subMap[fileID] = append(subMap[fileID], sub)
				}
			}
		}
	}

	result := make([]FileWithSubtitles, len(files))
	for i, f := range files {
		subs := subMap[f.ID]
		if subs == nil {
			subs = scanSubtitleFiles(f)
		}
		result[i] = FileWithSubtitles{
			MediaFile: f,
			Subtitles: subs,
		}
	}
	return result
}

// buildFilesWithSubtitles creates FileWithSubtitles entries with scanned subtitle files.
func buildFilesWithSubtitles(files []MediaFile) []FileWithSubtitles {
	if len(files) == 0 {
		return []FileWithSubtitles{}
	}

	result := make([]FileWithSubtitles, len(files))
	for i, f := range files {
		subs := scanSubtitleFiles(f)
		result[i] = FileWithSubtitles{
			MediaFile: f,
			Subtitles: subs,
		}
	}
	return result
}

// scanSubtitleFiles looks for .srt sidecar files next to the media file.
func scanSubtitleFiles(f MediaFile) []SubtitleInfo {
	if f.Path == "" {
		return nil
	}

	dir := filepath.Dir(f.Path)
	base := strings.TrimSuffix(f.FileName, filepath.Ext(f.FileName))

	pattern := filepath.Join(filepath.Clean(dir), base+"*.srt")
	matches, err := filepath.Glob(pattern)
	if err != nil {
		return nil
	}

	var subs []SubtitleInfo
	for _, match := range matches {
		filename := filepath.Base(match)
		langCode, hi, forced := parseSubtitleFilename(filename, base)
		if langCode == "" {
			continue
		}

		source := "downloaded"
		if hi || forced {
			source = "extracted"
		}

		subs = append(subs, SubtitleInfo{
			FileName:     filename,
			Language:     langCode,
			LanguageCode: langCode,
			HI:           hi,
			Forced:       forced,
			Source:       source,
		})
	}
	return subs
}

// parseSubtitleFilename extracts language code and flags from a subtitle filename.
// Pattern: basename.lang[.sdh|.forced].srt
func parseSubtitleFilename(filename, baseName string) (langCode string, hi bool, forced bool) {
	remainder := filename
	if strings.HasPrefix(filename, baseName+".") {
		remainder = filename[len(baseName)+1:]
	}
	remainder = strings.TrimSuffix(remainder, ".srt")

	parts := strings.Split(remainder, ".")
	if len(parts) == 0 {
		return "", false, false
	}

	langCode = parts[0]
	if langCode == "" {
		return "", false, false
	}

	for _, part := range parts[1:] {
		switch strings.ToLower(part) {
		case "sdh", "hi":
			hi = true
		case "forced":
			forced = true
		}
	}

	return langCode, hi, forced
}
