package service

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

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

type SubtitleSearchResult struct {
	ID            string `json:"id"`
	FileName      string `json:"file_name"`
	Language      string `json:"language"`
	LanguageCode  string `json:"language_code"`
	HI            bool   `json:"hi"`
	Forced        bool   `json:"forced"`
	DownloadCount int    `json:"download_count"`
	ReleaseName   string `json:"release_name"`
	Provider      string `json:"provider"`
}

type SubtitleFile struct {
	Path         string `json:"path"`
	Language     string `json:"language"`
	LanguageCode string `json:"language_code"`
	HI           bool   `json:"hi"`
	Forced       bool   `json:"forced"`
	Source       string `json:"source"`
}

type SubtitleSearchOptions struct {
	LanguageCodes []string
	HI            bool
	Forced        bool
}

type SubtitleService struct {
	db          *db.DB
	apiKey      string
	baseURL     string
	httpClient  *http.Client
	ffmpegPath  string
	ffprobePath string
}

func NewSubtitleService(database *db.DB, apiKey string) *SubtitleService {
	return &SubtitleService{
		db:      database,
		apiKey:  apiKey,
		baseURL: "https://api.opensubtitles.com/api/v1",
		httpClient: &http.Client{
			Timeout: 15 * time.Second,
		},
		ffmpegPath:  "ffmpeg",
		ffprobePath: "ffprobe",
	}
}

type osSearchResponse struct {
	TotalPages int          `json:"total_pages"`
	Data       []osSubtitle `json:"data"`
}

type osSubtitle struct {
	ID                string `json:"id"`
	FileName          string `json:"file_name"`
	Language          string `json:"language"`
	MovieFileNameMatch string `json:"movie_file_name_match"`
	DownloadCount     int    `json:"download_count"`
	HearingImpaired   bool   `json:"hearing_impaired"`
	ForeignPartsOnly  bool   `json:"foreign_parts_only"`
	ReleaseName       string `json:"release_name"`
	MovieHash         string `json:"movie_hash"`
}

type osDownloadResponse struct {
	Link     string `json:"link"`
	FileName string `json:"file_name"`
}

type osLoginResponse struct {
	Token string `json:"token"`
}

type ffprobeStream struct {
	Streams []ffprobeStreamInfo `json:"streams"`
}

type ffprobeStreamInfo struct {
	Index       int               `json:"index"`
	CodecName   string            `json:"codec_name"`
	CodecType   string            `json:"codec_type"`
	Disposition ffprobeDisposition `json:"disposition"`
	Tags        ffprobeTags       `json:"tags"`
}

type ffprobeDisposition struct {
	Forced int `json:"forced"`
}

type ffprobeTags struct {
	Language string `json:"language"`
	Title    string `json:"title"`
}

func (s *SubtitleService) osLogin(ctx context.Context) (string, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.baseURL+"/login", nil)
	if err != nil {
		return "", fmt.Errorf("create login request: %w", err)
	}
	req.Header.Set("Api-Key", s.apiKey)
	req.Header.Set("Content-Type", "application/json")

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

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

	var loginResp osLoginResponse
	if err := json.NewDecoder(resp.Body).Decode(&loginResp); err != nil {
		return "", fmt.Errorf("decode login response: %w", err)
	}

	return loginResp.Token, nil
}

func (s *SubtitleService) Search(ctx context.Context, query string, opts SubtitleSearchOptions) ([]SubtitleSearchResult, error) {
	token, err := s.osLogin(ctx)
	if err != nil {
		return nil, fmt.Errorf("opensubtitles login: %w", err)
	}

	langs := strings.Join(opts.LanguageCodes, ",")
	url := fmt.Sprintf("%s/subtitles?query=%s&languages=%s", s.baseURL, query, langs)
	if opts.HI {
		url += "&hearing_impaired=true"
	}
	if opts.Forced {
		url += "&foreign_parts_only=true"
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return nil, fmt.Errorf("create search request: %w", err)
	}
	req.Header.Set("Api-Key", s.apiKey)
	req.Header.Set("Authorization", "Bearer "+token)

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

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

	var searchResp osSearchResponse
	if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil {
		return nil, fmt.Errorf("decode search response: %w", err)
	}

	var results []SubtitleSearchResult
	for _, sub := range searchResp.Data {
		langCode := ""
		if parts := strings.Split(sub.Language, "-"); len(parts) > 0 {
			langCode = strings.ToLower(parts[0])
		}

		results = append(results, SubtitleSearchResult{
			ID:            sub.ID,
			FileName:      sub.FileName,
			Language:      sub.Language,
			LanguageCode:  langCode,
			HI:            sub.HearingImpaired,
			Forced:        sub.ForeignPartsOnly,
			DownloadCount: sub.DownloadCount,
			ReleaseName:   sub.ReleaseName,
			Provider:      "opensubtitles",
		})
	}

	return results, nil
}

func (s *SubtitleService) Download(ctx context.Context, subtitleID string, targetDir string, baseName string, langCode string, hi bool, forced bool) (*SubtitleFile, error) {
	token, err := s.osLogin(ctx)
	if err != nil {
		return nil, fmt.Errorf("opensubtitles login: %w", err)
	}

	body, _ := json.Marshal(map[string]string{"file_id": subtitleID})
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.baseURL+"/download", bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("create download request: %w", err)
	}
	req.Header.Set("Api-Key", s.apiKey)
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Content-Type", "application/json")

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

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

	var downloadResp osDownloadResponse
	if err := json.NewDecoder(resp.Body).Decode(&downloadResp); err != nil {
		return nil, fmt.Errorf("decode download response: %w", err)
	}

	fileReq, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadResp.Link, nil)
	if err != nil {
		return nil, fmt.Errorf("create file download request: %w", err)
	}

	fileResp, err := s.httpClient.Do(fileReq)
	if err != nil {
		return nil, fmt.Errorf("download subtitle file: %w", err)
	}
	defer fileResp.Body.Close()

	if fileResp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("subtitle file download failed: status %d", fileResp.StatusCode)
	}

	filename := baseName + "." + langCode + ".srt"
	if hi {
		filename = baseName + "." + langCode + ".sdh.srt"
	}
	if forced {
		filename = baseName + "." + langCode + ".forced.srt"
	}

	if err := os.MkdirAll(targetDir, 0755); err != nil {
		return nil, fmt.Errorf("create target directory: %w", err)
	}

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

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

	return &SubtitleFile{
		Path:         destPath,
		Language:     langCode,
		LanguageCode: langCode,
		HI:           hi,
		Forced:       forced,
		Source:       "downloaded",
	}, nil
}

func (s *SubtitleService) ExtractSubtitles(ctx context.Context, mediaFilePath string, targetDir string, baseName string) ([]SubtitleFile, error) {
	probeCtx, probeCancel := context.WithTimeout(ctx, 30*time.Second)
	defer probeCancel()

	cmd := exec.CommandContext(probeCtx, s.ffprobePath,
		"-v", "quiet",
		"-print_format", "json",
		"-show_streams",
		"-select_streams", "s",
		mediaFilePath,
	)

	var stdout bytes.Buffer
	cmd.Stdout = &stdout
	if err := cmd.Run(); err != nil {
		return nil, fmt.Errorf("ffprobe subtitle streams: %w", err)
	}

	var probeResult ffprobeStream
	if err := json.Unmarshal(stdout.Bytes(), &probeResult); err != nil {
		return nil, fmt.Errorf("parse ffprobe output: %w", err)
	}

	if len(probeResult.Streams) == 0 {
		return nil, nil
	}

	var results []SubtitleFile
	for i, stream := range probeResult.Streams {
		langCode := strings.ToLower(stream.Tags.Language)
		if langCode == "" {
			langCode = "und"
		}

		hi := false
		if strings.Contains(strings.ToLower(stream.Tags.Title), "sdh") {
			hi = true
		}

		forced := stream.Disposition.Forced == 1

		filename := baseName + "." + langCode + ".srt"
		if hi {
			filename = baseName + "." + langCode + ".sdh.srt"
		}
		if forced {
			filename = baseName + "." + langCode + ".forced.srt"
		}

		outputPath := filepath.Join(targetDir, filename)

		extractCtx, extractCancel := context.WithTimeout(ctx, 30*time.Second)
		extractCmd := exec.CommandContext(extractCtx, s.ffmpegPath,
			"-i", mediaFilePath,
			"-map", fmt.Sprintf("0:s:%d", i),
			"-f", "srt",
			outputPath,
		)
		if err := extractCmd.Run(); err != nil {
			slog.Error("failed to extract subtitle stream", "error", err, "stream_index", i)
			extractCancel()
			continue
		}
		extractCancel()

		results = append(results, SubtitleFile{
			Path:         outputPath,
			Language:     langCode,
			LanguageCode: langCode,
			HI:           hi,
			Forced:       forced,
			Source:       "extracted",
		})
	}

	return results, nil
}

func BuildSubtitleBaseName(title string, year *int, season, episode int) string {
	parts := []string{sanitizeSubtitleName(title)}
	if year != nil {
		parts = append(parts, fmt.Sprintf("%d", *year))
	}
	if season > 0 && episode > 0 {
		parts = append(parts, fmt.Sprintf("S%02dE%02d", season, episode))
	}
	return strings.Join(parts, ".")
}

var nonAlphaNumRe = regexp.MustCompile(`[^a-zA-Z0-9]+`)

func sanitizeSubtitleName(s string) string {
	s = nonAlphaNumRe.ReplaceAllString(s, ".")
	for strings.Contains(s, "..") {
		s = strings.ReplaceAll(s, "..", ".")
	}
	return strings.Trim(s, ".")
}
