package migrate

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

	"github.com/jackc/pgx/v5"
)

// importProwlarr reads Prowlarr indexers and imports them into UMM.
func (m *Migrator) importProwlarr(ctx context.Context) error {
	reader, err := NewArrReader(m.sources.Prowlarr)
	if err != nil {
		return fmt.Errorf("open prowlarr db: %w", err)
	}
	defer reader.Close()

	indexers, err := reader.ReadProwlarrIndexers()
	if err != nil {
		return fmt.Errorf("read prowlarr indexers: %w", err)
	}

	for _, idx := range indexers {
		if !idx.Enable {
			m.report.Indexers.Skipped++
			continue
		}

		url, apiKey := ParseIndexerSettings(idx.Settings)
		if url == "" {
			m.report.Indexers.Skipped++
			continue
		}

		impl := idx.Implementation
		if impl == "" {
			impl = "torznab"
		}

		var apiKeyPtr *string
		if apiKey != "" {
			apiKeyPtr = &apiKey
		}

		var id int64
		err := m.db.Pool.QueryRow(ctx, `
			INSERT INTO indexers (name, implementation, url, api_key, enabled, priority)
			VALUES ($1, $2, $3, $4, true, $5)
			ON CONFLICT (name) DO NOTHING
			RETURNING id`,
			idx.Name, impl, url, apiKeyPtr, idx.Priority).Scan(&id)
		if err != nil {
			if err == pgx.ErrNoRows {
				m.report.Indexers.Skipped++
				continue
			}
			slog.Error("failed to import indexer", "name", idx.Name, "error", err)
			m.report.Errors++
			continue
		}
		m.report.Indexers.Imported++
	}
	return nil
}

// importSonarr reads Sonarr series, quality profiles, root folders, tags, blocklist, and files.
func (m *Migrator) importSonarr(ctx context.Context, dbPath string, isAnime bool) error {
	reader, err := NewArrReader(dbPath)
	if err != nil {
		return fmt.Errorf("open sonarr db: %w", err)
	}
	defer reader.Close()

	// Import supporting data first
	profileMap, err := m.importQualityProfiles(ctx, reader, "series")
	if err != nil {
		slog.Error("failed to import sonarr quality profiles", "error", err)
		m.report.Errors++
	}
	folderMap, err := m.importRootFolders(ctx, reader, "series")
	if err != nil {
		slog.Error("failed to import sonarr root folders", "error", err)
		m.report.Errors++
	}
	tagMap, err := m.importTags(ctx, reader)
	if err != nil {
		slog.Error("failed to import sonarr tags", "error", err)
		m.report.Errors++
	}

	// Ensure anime tag exists if needed
	var animeTagID int64
	if isAnime {
		animeTagID, _ = m.ensureTag(ctx, "anime")
	}

	// Read and import series
	series, err := reader.ReadSonarrSeries()
	if err != nil {
		return fmt.Errorf("read sonarr series: %w", err)
	}

	seriesMap := m.sonarrSeriesMap
	if isAnime {
		seriesMap = m.sonarrAnimeSeriesMap
	}

	for _, s := range series {
		if s.TVDBID == 0 || s.Title == "" {
			m.report.Series.Skipped++
			continue
		}

		extIDs := fmt.Sprintf(`{"tvdb": %d}`, s.TVDBID)

		// Check for existing media with same external ID (for anime dedup)
		if isAnime {
			existingID, _, found := m.findMediaByExternalID(ctx, extIDs, "series")
			if found {
				// Add anime tag to existing item
				if animeTagID > 0 {
					m.addTagToMedia(ctx, existingID, "series", animeTagID)
				}
				m.report.Series.Skipped++
				slog.Debug("anime dedup: series already exists", "title", s.Title, "tvdb", s.TVDBID)
				continue
			}
		} else {
			// Check if already imported (non-anime)
			existingID, _, found := m.findMediaByExternalID(ctx, extIDs, "series")
			if found {
				seriesMap[s.ID] = existingID
				m.report.Series.Skipped++
				continue
			}
		}

		// Map quality profile and root folder
		qpID := m.mapProfileID(profileMap, s.QualityProfileID)
		rfID := m.mapFolderID(folderMap, s.RootFolderPath)

		// Determine status
		status := "unavailable"
		if s.Status == "continuing" || s.Status == "ended" {
			status = "unavailable" // will be updated to 'available' if files exist
		}

		sortTitle := s.SortTitle
		if sortTitle == "" {
			sortTitle = s.Title
		}

		var id int64
		err := m.db.Pool.QueryRow(ctx, `
			INSERT INTO media (media_type, title, sort_title, year, status, monitored,
				external_ids, metadata, images, quality_profile_id, root_folder_id)
			VALUES ('series', $1, $2, $3, $4, $5, $6::jsonb, '{}', $7::jsonb, $8, $9)
			ON CONFLICT DO NOTHING
			RETURNING id`,
			s.Title, sortTitle, zeroIfNil(s.Year), status, s.Monitored,
			extIDs, safeJSON(s.Images), qpID, rfID).Scan(&id)
		if err != nil {
			if err == pgx.ErrNoRows {
				m.report.Series.Skipped++
				continue
			}
			slog.Error("failed to import series", "title", s.Title, "error", err)
			m.report.Errors++
			continue
		}

		seriesMap[s.ID] = id

		// Add anime tag if needed
		if isAnime && animeTagID > 0 {
			m.addTagToMedia(ctx, id, "series", animeTagID)
		}

		// Map arr tags to UMM tags
		m.applyArrTags(ctx, s.Tags, id, "series", tagMap)

		m.report.Series.Imported++
	}

	// Import episode files
	files, err := reader.ReadEpisodeFiles()
	if err != nil {
		slog.Error("failed to read episode files", "error", err)
		m.report.Errors++
		return nil
	}

	for _, f := range files {
		ummSeriesID, ok := seriesMap[f.SeriesID]
		if !ok {
			continue
		}

		fileName := f.RelativePath
		if fileName == "" {
			fileName = filepath.Base(f.Path)
		}
		filePath := f.Path
		if filePath == "" {
			filePath = f.RelativePath
		}

		var fileID int64
		err := m.db.Pool.QueryRow(ctx, `
			INSERT INTO media_files (media_id, media_type, path, file_name, file_size, quality)
			VALUES ($1, 'series', $2, $3, $4, $5::jsonb)
			ON CONFLICT (media_id, media_type, path) DO NOTHING
			RETURNING id`,
			ummSeriesID, filePath, fileName, f.Size, safeJSON(f.Quality)).Scan(&fileID)
		if err != nil {
			if err == pgx.ErrNoRows {
				continue
			}
			slog.Error("failed to import episode file", "path", f.Path, "error", err)
			m.report.Errors++
			continue
		}
		m.report.Files.Imported++

		// Update series status to available
		m.db.Pool.Exec(ctx, `UPDATE media SET status = 'available' WHERE id = $1 AND media_type = 'series'`,
			ummSeriesID)
	}

	// Import blocklist
	m.importBlocklistFromReader(ctx, reader)

	return nil
}

// importRadarr reads Radarr movies, quality profiles, root folders, tags, blocklist, and files.
func (m *Migrator) importRadarr(ctx context.Context, dbPath string, isAnime bool) error {
	reader, err := NewArrReader(dbPath)
	if err != nil {
		return fmt.Errorf("open radarr db: %w", err)
	}
	defer reader.Close()

	// Import supporting data
	profileMap, err := m.importQualityProfiles(ctx, reader, "movie")
	if err != nil {
		slog.Error("failed to import radarr quality profiles", "error", err)
		m.report.Errors++
	}
	folderMap, err := m.importRootFolders(ctx, reader, "movie")
	if err != nil {
		slog.Error("failed to import radarr root folders", "error", err)
		m.report.Errors++
	}
	tagMap, err := m.importTags(ctx, reader)
	if err != nil {
		slog.Error("failed to import radarr tags", "error", err)
		m.report.Errors++
	}

	var animeTagID int64
	if isAnime {
		animeTagID, _ = m.ensureTag(ctx, "anime")
	}

	// Read and import movies
	movies, err := reader.ReadRadarrMovies()
	if err != nil {
		return fmt.Errorf("read radarr movies: %w", err)
	}

	movieMap := m.radarrMovieMap
	if isAnime {
		movieMap = m.radarrAnimeMovieMap
	}

	for _, mv := range movies {
		if mv.TMDBID == 0 || mv.Title == "" {
			m.report.Movies.Skipped++
			continue
		}

		extIDs := fmt.Sprintf(`{"tmdb": %d}`, mv.TMDBID)

		// Check for existing media with same external ID
		if isAnime {
			existingID, _, found := m.findMediaByExternalID(ctx, extIDs, "movie")
			if found {
				if animeTagID > 0 {
					m.addTagToMedia(ctx, existingID, "movie", animeTagID)
				}
				m.report.Movies.Skipped++
				slog.Debug("anime dedup: movie already exists", "title", mv.Title, "tmdb", mv.TMDBID)
				continue
			}
		} else {
			existingID, _, found := m.findMediaByExternalID(ctx, extIDs, "movie")
			if found {
				movieMap[mv.ID] = existingID
				m.report.Movies.Skipped++
				continue
			}
		}

		qpID := m.mapProfileID(profileMap, mv.QualityProfileID)
		rfID := m.mapFolderID(folderMap, mv.RootFolderPath)

		status := "unavailable"
		if mv.HasFile {
			status = "available"
		}

		sortTitle := mv.SortTitle
		if sortTitle == "" {
			sortTitle = mv.Title
		}

		var id int64
		err := m.db.Pool.QueryRow(ctx, `
			INSERT INTO media (media_type, title, sort_title, year, status, monitored,
				external_ids, metadata, images, quality_profile_id, root_folder_id)
			VALUES ('movie', $1, $2, $3, $4, $5, $6::jsonb, '{}', $7::jsonb, $8, $9)
			ON CONFLICT DO NOTHING
			RETURNING id`,
			mv.Title, sortTitle, zeroIfNil(mv.Year), status, mv.Monitored,
			extIDs, safeJSON(mv.Images), qpID, rfID).Scan(&id)
		if err != nil {
			if err == pgx.ErrNoRows {
				m.report.Movies.Skipped++
				continue
			}
			slog.Error("failed to import movie", "title", mv.Title, "error", err)
			m.report.Errors++
			continue
		}

		movieMap[mv.ID] = id

		if isAnime && animeTagID > 0 {
			m.addTagToMedia(ctx, id, "movie", animeTagID)
		}

		m.applyArrTags(ctx, "", id, "movie", tagMap)

		m.report.Movies.Imported++
	}

	// Import movie files
	files, err := reader.ReadMovieFiles()
	if err != nil {
		slog.Error("failed to read movie files", "error", err)
		m.report.Errors++
		return nil
	}

	for _, f := range files {
		ummMovieID, ok := movieMap[f.MovieID]
		if !ok {
			continue
		}

		fileName := f.RelativePath
		if fileName == "" {
			fileName = filepath.Base(f.Path)
		}
		filePath := f.Path
		if filePath == "" {
			filePath = f.RelativePath
		}

		var fileID int64
		err := m.db.Pool.QueryRow(ctx, `
			INSERT INTO media_files (media_id, media_type, path, file_name, file_size, quality)
			VALUES ($1, 'movie', $2, $3, $4, $5::jsonb)
			ON CONFLICT (media_id, media_type, path) DO NOTHING
			RETURNING id`,
			ummMovieID, filePath, fileName, f.Size, safeJSON(f.Quality)).Scan(&fileID)
		if err != nil {
			if err == pgx.ErrNoRows {
				continue
			}
			slog.Error("failed to import movie file", "path", f.Path, "error", err)
			m.report.Errors++
			continue
		}
		m.report.Files.Imported++

		// Update movie status to available
		m.db.Pool.Exec(ctx, `UPDATE media SET status = 'available' WHERE id = $1 AND media_type = 'movie'`,
			ummMovieID)
	}

	// Import blocklist
	m.importBlocklistFromReader(ctx, reader)

	return nil
}

// importLidarr reads Lidarr artists and albums.
func (m *Migrator) importLidarr(ctx context.Context) error {
	reader, err := NewArrReader(m.sources.Lidarr)
	if err != nil {
		return fmt.Errorf("open lidarr db: %w", err)
	}
	defer reader.Close()

	// Import supporting data
	_, err = m.importQualityProfiles(ctx, reader, "album")
	if err != nil {
		slog.Error("failed to import lidarr quality profiles", "error", err)
		m.report.Errors++
	}
	folderMap, err := m.importRootFolders(ctx, reader, "album")
	if err != nil {
		slog.Error("failed to import lidarr root folders", "error", err)
		m.report.Errors++
	}
	_, err = m.importTags(ctx, reader)
	if err != nil {
		slog.Error("failed to import lidarr tags", "error", err)
		m.report.Errors++
	}

	// Read artists (used as parent entities)
	artists, err := reader.ReadLidarrArtists()
	if err != nil {
		return fmt.Errorf("read lidarr artists: %w", err)
	}

	artistMap := make(map[int64]int64) // lidarr artist ID → UMM media ID
	for _, a := range artists {
		if a.ForeignArtistID == "" || a.Name == "" {
			continue
		}

		extIDs := fmt.Sprintf(`{"musicbrainz": "%s"}`, a.ForeignArtistID)

		// Check if already exists
		existingID, _, found := m.findMediaByExternalID(ctx, extIDs, "music")
		if found {
			artistMap[a.ID] = existingID
			continue
		}

		rfID := m.mapFolderID(folderMap, a.RootFolderPath)

		var id int64
		err := m.db.Pool.QueryRow(ctx, `
			INSERT INTO media (media_type, title, sort_title, status, monitored,
				external_ids, metadata, images, root_folder_id)
			VALUES ('music', $1, $1, $2, $3, $4::jsonb, '{}', $5::jsonb, $6)
			ON CONFLICT DO NOTHING
			RETURNING id`,
			a.Name, statusFromMonitored(a.Monitored), a.Monitored,
			extIDs, safeJSON(a.Images), rfID).Scan(&id)
		if err != nil {
			if err == pgx.ErrNoRows {
				continue
			}
			slog.Error("failed to import artist", "name", a.Name, "error", err)
			m.report.Errors++
			continue
		}
		artistMap[a.ID] = id
	}

	// Read albums
	albums, err := reader.ReadLidarrAlbums()
	if err != nil {
		return fmt.Errorf("read lidarr albums: %w", err)
	}

	for _, al := range albums {
		if al.ForeignAlbumID == "" || al.Title == "" {
			m.report.Albums.Skipped++
			continue
		}

		extIDs := fmt.Sprintf(`{"musicbrainz": "%s"}`, al.ForeignAlbumID)

		existingID, _, found := m.findMediaByExternalID(ctx, extIDs, "album")
		if found {
			m.lidarrAlbumMap[al.ID] = existingID
			m.report.Albums.Skipped++
			continue
		}

		var id int64
		err := m.db.Pool.QueryRow(ctx, `
			INSERT INTO media (media_type, title, sort_title, year, status, monitored,
				external_ids, metadata, images)
			VALUES ('album', $1, $1, $2, $3, $4, $5::jsonb, '{}', '[]')
			ON CONFLICT DO NOTHING
			RETURNING id`,
			al.Title, zeroIfNil(al.Year), statusFromMonitored(al.Monitored),
			al.Monitored, extIDs).Scan(&id)
		if err != nil {
			if err == pgx.ErrNoRows {
				m.report.Albums.Skipped++
				continue
			}
			slog.Error("failed to import album", "title", al.Title, "error", err)
			m.report.Errors++
			continue
		}

		m.lidarrAlbumMap[al.ID] = id

		// Create relation: artist → album
		if artistID, ok := artistMap[al.ArtistID]; ok {
			m.db.Pool.Exec(ctx, `
				INSERT INTO media_relations (parent_id, child_id, relation)
				VALUES ($1, $2, 'artist_album')
				ON CONFLICT (parent_id, child_id, relation) DO NOTHING`,
				artistID, id)
		}

		m.report.Albums.Imported++
	}

	// Import track files
	trackFiles, err := reader.ReadTrackFiles()
	if err != nil {
		slog.Error("failed to read track files", "error", err)
		m.report.Errors++
		return nil
	}

	for _, tf := range trackFiles {
		ummAlbumID, ok := m.lidarrAlbumMap[tf.AlbumID]
		if !ok {
			continue
		}

		fileName := tf.RelativePath
		if fileName == "" {
			fileName = filepath.Base(tf.Path)
		}
		filePath := tf.Path
		if filePath == "" {
			filePath = tf.RelativePath
		}

		var fileID int64
		err := m.db.Pool.QueryRow(ctx, `
			INSERT INTO media_files (media_id, media_type, path, file_name, file_size, quality)
			VALUES ($1, 'album', $2, $3, $4, $5::jsonb)
			ON CONFLICT (media_id, media_type, path) DO NOTHING
			RETURNING id`,
			ummAlbumID, filePath, fileName, tf.Size, safeJSON(tf.Quality)).Scan(&fileID)
		if err != nil {
			if err == pgx.ErrNoRows {
				continue
			}
			continue
		}
		m.report.Files.Imported++

		m.db.Pool.Exec(ctx, `UPDATE media SET status = 'available' WHERE id = $1 AND media_type = 'album'`,
			ummAlbumID)
	}

	return nil
}

// importReadarr reads Readarr books.
func (m *Migrator) importReadarr(ctx context.Context) error {
	reader, err := NewArrReader(m.sources.Readarr)
	if err != nil {
		return fmt.Errorf("open readarr db: %w", err)
	}
	defer reader.Close()

	// Import supporting data
	_, err = m.importQualityProfiles(ctx, reader, "other")
	if err != nil {
		slog.Error("failed to import readarr quality profiles", "error", err)
		m.report.Errors++
	}
	folderMap, err := m.importRootFolders(ctx, reader, "other")
	if err != nil {
		slog.Error("failed to import readarr root folders", "error", err)
		m.report.Errors++
	}
	_, err = m.importTags(ctx, reader)
	if err != nil {
		slog.Error("failed to import readarr tags", "error", err)
		m.report.Errors++
	}

	books, err := reader.ReadReadarrBooks()
	if err != nil {
		return fmt.Errorf("read readarr books: %w", err)
	}

	for _, b := range books {
		if b.Title == "" {
			m.report.Books.Skipped++
			continue
		}

		// Build external IDs — could be ISBN, Goodreads, or other
		extIDs := "{}"
		if b.ForeignBookID != "" {
			if isISBN(b.ForeignBookID) {
				extIDs = fmt.Sprintf(`{"isbn": "%s"}`, b.ForeignBookID)
			} else {
				extIDs = fmt.Sprintf(`{"goodreads": "%s"}`, b.ForeignBookID)
			}
		}

		// Check if already exists
		if extIDs != "{}" {
			_, _, found := m.findMediaByExternalID(ctx, extIDs, "other")
			if found {
				m.report.Books.Skipped++
				continue
			}
		}

		title := b.Title
		if b.AuthorName != "" {
			title = fmt.Sprintf("%s - %s", b.AuthorName, b.Title)
		}

		rfID := m.mapFolderID(folderMap, "")

		var id int64
		err := m.db.Pool.QueryRow(ctx, `
			INSERT INTO media (media_type, title, sort_title, status, monitored,
				external_ids, metadata, images, root_folder_id)
			VALUES ('other', $1, $1, $2, $3, $4::jsonb, '{}', $5::jsonb, $6)
			ON CONFLICT DO NOTHING
			RETURNING id`,
			title, statusFromMonitored(b.Monitored), b.Monitored,
			extIDs, safeJSON(b.Images), rfID).Scan(&id)
		if err != nil {
			if err == pgx.ErrNoRows {
				m.report.Books.Skipped++
				continue
			}
			slog.Error("failed to import book", "title", b.Title, "error", err)
			m.report.Errors++
			continue
		}

		m.readarrBookMap[b.ID] = id
		m.report.Books.Imported++
	}

	// Import book files
	bookFiles, err := reader.ReadBookFiles()
	if err != nil {
		slog.Error("failed to read book files", "error", err)
		m.report.Errors++
		return nil
	}

	for _, bf := range bookFiles {
		ummBookID, ok := m.readarrBookMap[bf.BookID]
		if !ok {
			continue
		}

		fileName := bf.RelativePath
		if fileName == "" {
			fileName = filepath.Base(bf.Path)
		}
		filePath := bf.Path
		if filePath == "" {
			filePath = bf.RelativePath
		}

		var fileID int64
		err := m.db.Pool.QueryRow(ctx, `
			INSERT INTO media_files (media_id, media_type, path, file_name, file_size, quality)
			VALUES ($1, 'other', $2, $3, $4, $5::jsonb)
			ON CONFLICT (media_id, media_type, path) DO NOTHING
			RETURNING id`,
			ummBookID, filePath, fileName, bf.Size, safeJSON(bf.Quality)).Scan(&fileID)
		if err != nil {
			if err == pgx.ErrNoRows {
				continue
			}
			continue
		}
		m.report.Files.Imported++

		m.db.Pool.Exec(ctx, `UPDATE media SET status = 'available' WHERE id = $1 AND media_type = 'other'`,
			ummBookID)
	}

	return nil
}

// resetSequences resets PostgreSQL sequences to match the max ID in each table.
func (m *Migrator) resetSequences(ctx context.Context) error {
	sequences := []struct {
		seq   string
		table string
	}{
		{"media_id_seq", "media"},
		{"media_files_id_seq", "media_files"},
		{"media_relations_id_seq", "media_relations"},
		{"download_queue_id_seq", "download_queue"},
		{"blocklist_id_seq", "blocklist"},
		{"requests_id_seq", "requests"},
	}

	for _, s := range sequences {
		_, err := m.db.Pool.Exec(ctx, fmt.Sprintf(
			`SELECT setval('%s', COALESCE((SELECT MAX(id) FROM %s), 0))`, s.seq, s.table))
		if err != nil {
			slog.Error("failed to reset sequence", "sequence", s.seq, "error", err)
			// Not fatal — continue with other sequences
		} else {
			slog.Debug("reset sequence", "sequence", s.seq)
		}
	}
	return nil
}

// --- Helper methods ---

// ensureTag creates a tag if it doesn't exist and returns its ID.
func (m *Migrator) ensureTag(ctx context.Context, name string) (int64, error) {
	// Try to find existing
	var id int64
	err := m.db.Pool.QueryRow(ctx,
		`SELECT id FROM tags WHERE name = $1`, name).Scan(&id)
	if err == nil {
		return id, nil
	}

	// Create new
	err = m.db.Pool.QueryRow(ctx,
		`INSERT INTO tags (name, color) VALUES ($1, '#ef4444') ON CONFLICT (name) DO NOTHING RETURNING id`,
		name).Scan(&id)
	if err != nil {
		if err == pgx.ErrNoRows {
			// Race condition — someone else created it
			err = m.db.Pool.QueryRow(ctx,
				`SELECT id FROM tags WHERE name = $1`, name).Scan(&id)
		}
		if err != nil {
			return 0, fmt.Errorf("ensure tag %s: %w", name, err)
		}
	}
	m.report.Tags.Imported++
	return id, nil
}

// importQualityProfiles imports quality profiles from an arr database and returns a mapping
// from arr profile ID to UMM profile ID.
func (m *Migrator) importQualityProfiles(ctx context.Context, reader *ArrReader, mediaType string) (map[int64]int64, error) {
	profiles, err := reader.ReadQualityProfiles()
	if err != nil {
		return nil, err
	}

	profileMap := make(map[int64]int64)
	for _, p := range profiles {
		items := ParseQualityItems(p.Items)
		allowed := ExtractAllowedQualities(items)
		cutoffName := FindCutoffName(items, p.Cutoff)

		if len(allowed) == 0 {
			continue
		}

		// Build JSON arrays
		mtJSON, _ := json.Marshal([]string{mediaType})
		cutoffJSON, _ := json.Marshal(cutoffName)
		allowedJSON, _ := json.Marshal(allowed)

		var id int64
		err := m.db.Pool.QueryRow(ctx, `
			INSERT INTO quality_profiles (name, media_types, cutoff_quality, allowed_qualities)
			VALUES ($1, $2, $3, $4)
			ON CONFLICT DO NOTHING
			RETURNING id`,
			p.Name, mtJSON, cutoffJSON, allowedJSON).Scan(&id)
		if err != nil {
			if err == pgx.ErrNoRows {
				// Profile with this name already exists — look it up
				err = m.db.Pool.QueryRow(ctx,
					`SELECT id FROM quality_profiles WHERE name = $1`, p.Name).Scan(&id)
				if err != nil {
					continue
				}
			} else {
				slog.Error("failed to import quality profile", "name", p.Name, "error", err)
				continue
			}
		} else {
			m.report.Profiles.Imported++
		}

		profileMap[p.ID] = id
	}
	return profileMap, nil
}

// importRootFolders imports root folders from an arr database and returns a mapping
// from arr root folder path to UMM root folder ID.
func (m *Migrator) importRootFolders(ctx context.Context, reader *ArrReader, mediaType string) (map[string]int64, error) {
	folders, err := reader.ReadRootFolders()
	if err != nil {
		return nil, err
	}

	folderMap := make(map[string]int64)
	for _, f := range folders {
		if f.Path == "" {
			continue
		}

		var id int64
		err := m.db.Pool.QueryRow(ctx, `
			INSERT INTO root_folders (path, media_type)
			VALUES ($1, $2)
			ON CONFLICT (path) DO NOTHING
			RETURNING id`,
			f.Path, mediaType).Scan(&id)
		if err != nil {
			if err == pgx.ErrNoRows {
				err = m.db.Pool.QueryRow(ctx,
					`SELECT id FROM root_folders WHERE path = $1`, f.Path).Scan(&id)
				if err != nil {
					continue
				}
			} else {
				continue
			}
		} else {
			m.report.RootFolders.Imported++
		}

		folderMap[f.Path] = id
	}
	return folderMap, nil
}

// importTags imports tags from an arr database and returns a mapping from arr tag ID to UMM tag ID.
func (m *Migrator) importTags(ctx context.Context, reader *ArrReader) (map[int64]int64, error) {
	tags, err := reader.ReadTags()
	if err != nil {
		return nil, err
	}

	tagMap := make(map[int64]int64)
	for _, t := range tags {
		if t.Label == "" {
			continue
		}

		var id int64
		err := m.db.Pool.QueryRow(ctx, `
			INSERT INTO tags (name) VALUES ($1)
			ON CONFLICT (name) DO NOTHING
			RETURNING id`,
			t.Label).Scan(&id)
		if err != nil {
			if err == pgx.ErrNoRows {
				err = m.db.Pool.QueryRow(ctx,
					`SELECT id FROM tags WHERE name = $1`, t.Label).Scan(&id)
				if err != nil {
					continue
				}
			} else {
				continue
			}
		} else {
			m.report.Tags.Imported++
		}

		tagMap[t.ID] = id
	}
	return tagMap, nil
}

// importBlocklistFromReader imports blocklist entries from an arr database.
func (m *Migrator) importBlocklistFromReader(ctx context.Context, reader *ArrReader) {
	entries, err := reader.ReadBlocklist()
	if err != nil {
		slog.Error("failed to read blocklist", "error", err)
		return
	}

	for _, e := range entries {
		if e.Title == "" {
			continue
		}

		var sourceTitle *string
		if e.SourceTitle != "" {
			sourceTitle = &e.SourceTitle
		}
		var torrentHash *string
		if e.TorrentHash != "" {
			torrentHash = &e.TorrentHash
		}
		var message *string
		if e.Message != "" {
			message = &e.Message
		}
		var size *int64
		if e.Size > 0 {
			size = &e.Size
		}

		protocol := e.Protocol
		if protocol == "" {
			protocol = "torrent"
		}

		_, err := m.db.Pool.Exec(ctx, `
			INSERT INTO blocklist (release_title, source_title, quality, protocol,
				torrent_hash, size, message, block_reason)
			VALUES ($1, $2, $3::jsonb, $4, $5, $6, $7, 'imported')`,
			e.Title, sourceTitle, safeJSON(e.Quality), protocol,
			torrentHash, size, message)
		if err != nil {
			slog.Error("failed to import blocklist entry", "title", e.Title, "error", err)
			continue
		}
		m.report.Blocklist.Imported++
	}
}

// findMediaByExternalID checks if media with the given external IDs JSON exists.
// Returns the UMM media ID, media type, and whether it was found.
func (m *Migrator) findMediaByExternalID(ctx context.Context, extIDs string, mediaType string) (int64, string, bool) {
	var id int64
	var mt string
	err := m.db.Pool.QueryRow(ctx, `
		SELECT id, media_type FROM media
		WHERE external_ids @> $1::jsonb AND media_type = $2 AND deleted_at IS NULL
		LIMIT 1`,
		extIDs, mediaType).Scan(&id, &mt)
	if err != nil {
		return 0, "", false
	}
	return id, mt, true
}

// addTagToMedia adds a tag to a media item.
func (m *Migrator) addTagToMedia(ctx context.Context, mediaID int64, mediaType string, tagID int64) {
	_, err := m.db.Pool.Exec(ctx, `
		INSERT INTO media_tags (media_id, media_type, tag_id)
		VALUES ($1, $2, $3)
		ON CONFLICT (media_id, media_type, tag_id) DO NOTHING`,
		mediaID, mediaType, tagID)
	if err != nil {
		slog.Error("failed to add tag to media", "media_id", mediaID, "tag_id", tagID, "error", err)
	}
}

// applyArrTags applies arr tag IDs (from the JSON tags column) to UMM media.
func (m *Migrator) applyArrTags(ctx context.Context, tagsJSON string, mediaID int64, mediaType string, tagMap map[int64]int64) {
	if tagsJSON == "" || tagsJSON == "[]" {
		return
	}
	var tagIDs []int64
	if err := json.Unmarshal([]byte(tagsJSON), &tagIDs); err != nil {
		return
	}
	for _, arrTagID := range tagIDs {
		ummTagID, ok := tagMap[arrTagID]
		if !ok {
			continue
		}
		m.addTagToMedia(ctx, mediaID, mediaType, ummTagID)
	}
}

// mapProfileID maps an arr quality profile ID to a UMM quality profile ID.
func (m *Migrator) mapProfileID(profileMap map[int64]int64, arrID int64) *int64 {
	if profileMap == nil || arrID == 0 {
		return nil
	}
	if ummID, ok := profileMap[arrID]; ok {
		return &ummID
	}
	return nil
}

// mapFolderID maps an arr root folder path to a UMM root folder ID.
func (m *Migrator) mapFolderID(folderMap map[string]int64, path string) *int64 {
	if folderMap == nil || path == "" {
		return nil
	}
	if ummID, ok := folderMap[path]; ok {
		return &ummID
	}
	return nil
}

// safeJSON returns valid JSON or '{}' if the input is empty.
func safeJSON(s string) string {
	if s == "" || s == "null" {
		return "{}"
	}
	s = strings.TrimSpace(s)
	if !strings.HasPrefix(s, "{") && !strings.HasPrefix(s, "[") {
		return "{}"
	}
	return s
}

// zeroIfNil returns 0 for a zero value (for nullable ints).
func zeroIfNil(n int) *int {
	if n == 0 {
		return nil
	}
	return &n
}

// statusFromMonitored returns a media status based on the monitored flag.
func statusFromMonitored(monitored bool) string {
	if monitored {
		return "unavailable"
	}
	return "unavailable"
}

// isISBN checks if a string looks like an ISBN.
func isISBN(s string) bool {
	s = strings.ReplaceAll(s, "-", "")
	s = strings.ReplaceAll(s, " ", "")
	return len(s) == 10 || len(s) == 13
}
