package service

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

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

type ImportResult struct {
	MediaID    int64  `json:"media_id"`
	MediaType  string `json:"media_type"`
	SourcePath string `json:"source_path"`
	DestPath   string `json:"dest_path"`
	FileSize   int64  `json:"file_size"`
	Quality    string `json:"quality"`
	Status     string `json:"status"`
}

type ImportReport struct {
	Imported int            `json:"imported"`
	Skipped  int            `json:"skipped"`
	Errors   int            `json:"errors"`
	Results  []ImportResult `json:"results"`
}

type ImportService struct {
	db               *db.DB
	downloadClientSvc *DownloadClientService
	namingSvc        *NamingService
	matcherSvc       *MatcherService
	mediaSvc         *MediaService
	parser           *ReleaseParser
	downloadDir      string
	subtitleSvc      *SubtitleService
	activitySvc      *ActivityService
}

func NewImportService(database *db.DB, dcSvc *DownloadClientService, nSvc *NamingService, mSvc *MatcherService, mediaSvc *MediaService, downloadDir string, subtitleSvc *SubtitleService, activitySvc *ActivityService) *ImportService {
	return &ImportService{
		db:               database,
		downloadClientSvc: dcSvc,
		namingSvc:        nSvc,
		matcherSvc:       mSvc,
		mediaSvc:         mediaSvc,
		parser:           NewReleaseParser(),
		downloadDir:      downloadDir,
		subtitleSvc:      subtitleSvc,
		activitySvc:      activitySvc,
	}
}

var mediaExts = map[string]bool{
	".mkv":  true,
	".mp4":  true,
	".avi":  true,
	".wmv":  true,
	".flv":  true,
	".webm": true,
	".mp3":  true,
	".flac": true,
	".m4a":  true,
	".m4b":  true,
	".ogg":  true,
	".opus": true,
	".epub": true,
	".pdf":  true,
	".mobi": true,
	".azw3": true,
}

func (s *ImportService) ProcessCompleted(ctx context.Context) (*ImportReport, error) {
	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	report := &ImportReport{}

	nzbClients, err := s.downloadClientSvc.GetAllEnabled(ctx, "nzb")
	if err != nil {
		slog.Error("failed to get nzb clients", "error", err)
	}
	torrentClients, err := s.downloadClientSvc.GetAllEnabled(ctx, "torrent")
	if err != nil {
		slog.Error("failed to get torrent clients", "error", err)
	}

	allClients := append(nzbClients, torrentClients...)

	for _, client := range allClients {
		completed, err := client.Client.GetCompleted(ctx)
		if err != nil {
			slog.Error("failed to get completed downloads", "error", err, "client", client.Config.Name)
			continue
		}

		for _, dl := range completed {
			s.processDownload(ctx, dl, client, report)
		}
	}

	return report, nil
}

func (s *ImportService) processDownload(ctx context.Context, dl download.CompletedDownload, client DownloadClientWithInfo, report *ImportReport) {
	var exists bool
	err := s.db.Pool.QueryRow(ctx,
		"SELECT EXISTS(SELECT 1 FROM media_files WHERE original_path = $1 AND deleted_at IS NULL)",
		dl.OutputPath).Scan(&exists)
	if err != nil {
		slog.Error("failed to check existing import", "error", err, "path", dl.OutputPath)
		report.Errors++
		return
	}
	if exists {
		report.Skipped++
		return
	}

	files, err := s.findMediaFiles(dl.Name)
	if err != nil {
		slog.Error("failed to find media files", "error", err, "download", dl.Name)
		report.Errors++
		return
	}

	if len(files) == 0 {
		slog.Warn("no media files found for download", "download", dl.Name)
		report.Skipped++
		return
	}

	for _, filePath := range files {
		s.processFile(ctx, filePath, dl, client, report)
	}
}

func (s *ImportService) processFile(ctx context.Context, sourcePath string, dl download.CompletedDownload, client DownloadClientWithInfo, report *ImportReport) {
	fileCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()

	releaseInfo := s.parser.Parse(filepath.Base(sourcePath))

	mediaType := "movie"
	if _, _, hasSE := parseSeasonEpisode(filepath.Base(sourcePath)); hasSE {
		mediaType = "series"
	}

	match, err := s.matcherSvc.Match(fileCtx, dl.Name, mediaType)
	if err != nil {
		slog.Error("failed to match release to media", "error", err, "release", dl.Name)
		report.Errors++
		return
	}
	if match.Confidence == "none" {
		slog.Warn("no media match for release", "release", dl.Name, "path", sourcePath)
		report.Skipped++
		return
	}

	result, err := s.importFile(fileCtx, sourcePath, match, releaseInfo, dl, client)
	if err != nil {
		slog.Error("failed to import file", "error", err, "source", sourcePath)
		report.Errors++
		return
	}

	report.Imported++
	report.Results = append(report.Results, *result)
}

func (s *ImportService) importFile(ctx context.Context, sourcePath string, match *MatchResult, releaseInfo ReleaseInfo, completed download.CompletedDownload, client DownloadClientWithInfo) (*ImportResult, error) {
	status := "importing"
	err := s.mediaSvc.Update(ctx, match.MediaID, match.MediaType, UpdateMediaRequest{
		Status: &status,
	})
	if err != nil {
		return nil, fmt.Errorf("update media status to importing: %w", err)
	}

	qualityTier := s.parser.MatchQuality(releaseInfo)
	qualityJSON, _ := json.Marshal(qualityTier)

	year := 0
	if match.Year != nil {
		year = *match.Year
	}
	season := 0
	if match.Season != nil {
		season = *match.Season
	}
	episode := 0
	if match.Episode != nil {
		episode = *match.Episode
	}

	namingData := NamingData{
		Title:        match.Title,
		Year:         year,
		Season:       season,
		Episode:      episode,
		Quality:      qualityTier.Name,
		Ext:          ExtractExt(filepath.Base(sourcePath)),
		ReleaseGroup: releaseInfo.ReleaseGroup,
		Resolution:   releaseInfo.Resolution,
		Source:       releaseInfo.Source,
		Codec:        releaseInfo.VideoCodec,
	}

	relativePath, err := s.namingSvc.Render(ctx, match.MediaType, namingData)
	if err != nil {
		s.rollbackStatus(ctx, match.MediaID, match.MediaType, "failed")
		s.logImportError(match.MediaID, match.MediaType, fmt.Sprintf("Naming template failed for media %d: %v", match.MediaID, err))
		return nil, fmt.Errorf("render naming template: %w", err)
	}

	targetPath := filepath.Join(match.RootFolder, relativePath)

	if !strings.HasPrefix(filepath.Clean(targetPath), filepath.Clean(match.RootFolder)) {
		s.rollbackStatus(ctx, match.MediaID, match.MediaType, "failed")
		return nil, fmt.Errorf("path traversal detected: target path escapes root folder")
	}

	targetDir := filepath.Dir(targetPath)
	if err := os.MkdirAll(targetDir, 0755); err != nil {
		s.rollbackStatus(ctx, match.MediaID, match.MediaType, "failed")
		return nil, fmt.Errorf("create target directory: %w", err)
	}

	if err := os.Link(sourcePath, targetPath); err != nil {
		s.rollbackStatus(ctx, match.MediaID, match.MediaType, "failed")
		s.logImportError(match.MediaID, match.MediaType, fmt.Sprintf("Hardlink failed for media %d: %v", match.MediaID, err))
		return nil, fmt.Errorf("hardlink file: %w", err)
	}

	srcInfo, err := os.Stat(sourcePath)
	if err != nil {
		s.rollbackStatus(ctx, match.MediaID, match.MediaType, "failed")
		return nil, fmt.Errorf("stat source file: %w", err)
	}
	dstInfo, err := os.Stat(targetPath)
	if err != nil {
		s.rollbackStatus(ctx, match.MediaID, match.MediaType, "failed")
		return nil, fmt.Errorf("stat target file: %w", err)
	}
	if !os.SameFile(srcInfo, dstInfo) {
		s.rollbackStatus(ctx, match.MediaID, match.MediaType, "failed")
		return nil, fmt.Errorf("hardlink verification failed: files are not the same inode")
	}

	fileSize := dstInfo.Size()

	if s.subtitleSvc != nil {
		extractCtx, extractCancel := context.WithTimeout(ctx, 30*time.Second)
		baseName := s.buildImportSubtitleBaseName(match, releaseInfo)
		extracted, err := s.subtitleSvc.ExtractSubtitles(extractCtx, targetPath, filepath.Dir(targetPath), baseName)
		if err != nil {
			slog.Error("failed to extract subtitles", "error", err, "path", targetPath)
		}
		if len(extracted) > 0 {
			slog.Info("extracted subtitles", "count", len(extracted), "media_id", match.MediaID)
		}
		extractCancel()
	}

	_, err = s.db.Pool.Exec(ctx,
		`INSERT INTO media_files (media_id, media_type, path, original_path, file_name, file_size, quality, codec, resolution, source, is_hardlinked)
		 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
		match.MediaID, match.MediaType, targetPath, sourcePath, filepath.Base(targetPath),
		fileSize, qualityJSON, ptrStr(releaseInfo.VideoCodec), ptrStr(releaseInfo.Resolution),
		ptrStr(releaseInfo.Source), true)
	if err != nil {
		s.rollbackStatus(ctx, match.MediaID, match.MediaType, "failed")
		return nil, fmt.Errorf("insert media file record: %w", err)
	}

	availableStatus := "available"
	err = s.mediaSvc.Update(ctx, match.MediaID, match.MediaType, UpdateMediaRequest{
		Status:         &availableStatus,
		CurrentQuality: qualityJSON,
	})
	if err != nil {
		slog.Error("failed to update media status to available", "error", err, "media_id", match.MediaID)
	}

	if _, err := s.db.Pool.Exec(ctx, `UPDATE media SET has_files = true WHERE id = $1`, match.MediaID); err != nil {
		slog.Error("failed to update has_files", "error", err, "media_id", match.MediaID)
	}

	// Log successful import activity
	if s.activitySvc != nil {
		s.activitySvc.LogAsync(LogEntry{
			EventType: "import",
			MediaID:   &match.MediaID,
			MediaType: &match.MediaType,
			Title:     fmt.Sprintf("Imported %s", filepath.Base(sourcePath)),
			Data: json.RawMessage(fmt.Sprintf(`{"source":"%s","dest":"%s","quality":"%s","size":%d}`,
				sourcePath, targetPath, qualityTier.Name, fileSize)),
		})
	}

	_, err = s.db.Pool.Exec(ctx,
		`UPDATE download_queue SET status = 'imported', completed_at = NOW()
		 WHERE media_id = $1 AND release_title = $2 AND status IN ('downloading', 'pending')`,
		match.MediaID, completed.Name)
	if err != nil {
		slog.Error("failed to update download queue", "error", err, "media_id", match.MediaID)
	}

	if err := client.Client.Remove(ctx, completed.ID); err != nil {
		slog.Warn("failed to remove download client entry", "error", err, "id", completed.ID)
	}

	return &ImportResult{
		MediaID:    match.MediaID,
		MediaType:  match.MediaType,
		SourcePath: sourcePath,
		DestPath:   targetPath,
		FileSize:   fileSize,
		Quality:    qualityTier.Name,
		Status:     "imported",
	}, nil
}

func (s *ImportService) rollbackStatus(ctx context.Context, mediaID int64, mediaType string, status string) {
	if err := s.mediaSvc.Update(ctx, mediaID, mediaType, UpdateMediaRequest{Status: &status}); err != nil {
		slog.Error("failed to rollback media status", "error", err, "media_id", mediaID)
	}
}

func (s *ImportService) findMediaFiles(downloadName string) ([]string, error) {
	downloadPath := filepath.Join(s.downloadDir, downloadName)
	cleanBase := filepath.Clean(s.downloadDir)

	info, err := os.Stat(downloadPath)
	if err != nil {
		entries, err := os.ReadDir(s.downloadDir)
		if err != nil {
			return nil, fmt.Errorf("read download directory: %w", err)
		}
		for _, entry := range entries {
			candidate := filepath.Join(s.downloadDir, entry.Name())
			if strings.Contains(strings.ToLower(entry.Name()), strings.ToLower(downloadName)) {
				if entry.IsDir() {
					return s.walkMediaDir(candidate, cleanBase)
				}
				if mediaExts[filepath.Ext(entry.Name())] {
					return []string{candidate}, nil
				}
			}
		}
		return nil, nil
	}

	if !strings.HasPrefix(filepath.Clean(downloadPath), cleanBase) {
		return nil, fmt.Errorf("path traversal detected: download path escapes download dir")
	}

	if info.IsDir() {
		return s.walkMediaDir(downloadPath, cleanBase)
	}

	if mediaExts[filepath.Ext(downloadPath)] {
		return []string{downloadPath}, nil
	}

	return nil, nil
}

func (s *ImportService) walkMediaDir(dir string, cleanBase string) ([]string, error) {
	var files []string
	err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if !strings.HasPrefix(filepath.Clean(path), cleanBase) {
			return fmt.Errorf("path traversal detected: walked path escapes download dir")
		}
		if d.IsDir() {
			return nil
		}
		if mediaExts[filepath.Ext(path)] {
			files = append(files, path)
		}
		return nil
	})
	if err != nil {
		return nil, fmt.Errorf("walk download directory: %w", err)
	}
	return files, nil
}

func (s *ImportService) buildImportSubtitleBaseName(match *MatchResult, info ReleaseInfo) string {
	parts := []string{sanitize(match.Title)}
	if match.Year != nil {
		parts = append(parts, fmt.Sprintf("%d", *match.Year))
	}
	if match.Season != nil && match.Episode != nil {
		parts = append(parts, fmt.Sprintf("S%02dE%02d", *match.Season, *match.Episode))
	}
	return strings.Join(parts, ".")
}

func ptrStr(s string) *string {
	if s == "" {
		return nil
	}
	return &s
}

func (s *ImportService) logImportError(mediaID int64, mediaType string, msg string) {
	if s.activitySvc != nil {
		s.activitySvc.LogAsync(LogEntry{
			EventType: "error",
			Title:     msg,
			MediaID:   &mediaID,
			MediaType: &mediaType,
		})
	}
}
