package taglib

import (
	"io/fs"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/navidrome/navidrome/conf"
	"github.com/navidrome/navidrome/core/storage/local"
	"github.com/navidrome/navidrome/log"
	"github.com/navidrome/navidrome/model/metadata"
)

type extractor struct {
	baseDir string
}

func (e extractor) Parse(files ...string) (map[string]metadata.Info, error) {
	results := make(map[string]metadata.Info)
	for _, path := range files {
		props, err := e.extractMetadata(path)
		if err != nil {
			continue
		}
		results[path] = *props
	}
	return results, nil
}

func (e extractor) Version() string {
	return Version()
}

func (e extractor) extractMetadata(filePath string) (*metadata.Info, error) {
	fullPath := filepath.Join(e.baseDir, filePath)
	tags, err := Read(fullPath)
	if err != nil {
		log.Warn("extractor: Error reading metadata from file. Skipping", "filePath", fullPath, err)
		return nil, err
	}

	// Parse audio properties
	ap := metadata.AudioProperties{}
	ap.BitRate = parseProp(tags, "__bitrate")
	ap.Channels = parseProp(tags, "__channels")
	ap.SampleRate = parseProp(tags, "__samplerate")
	ap.BitDepth = parseProp(tags, "__bitspersample")
	length := parseProp(tags, "__lengthinmilliseconds")
	ap.Duration = (time.Millisecond * time.Duration(length)).Round(time.Millisecond * 10)

	// Extract basic tags
	parseBasicTag(tags, "__title", "title")
	parseBasicTag(tags, "__artist", "artist")
	parseBasicTag(tags, "__album", "album")
	parseBasicTag(tags, "__comment", "comment")
	parseBasicTag(tags, "__genre", "genre")
	parseBasicTag(tags, "__year", "year")
	parseBasicTag(tags, "__track", "tracknumber")

	// Parse track/disc totals
	parseTuple := func(prop string) {
		tagName := prop + "number"
		tagTotal := prop + "total"
		if value, ok := tags[tagName]; ok && len(value) > 0 {
			parts := strings.Split(value[0], "/")
			tags[tagName] = []string{parts[0]}
			if len(parts) == 2 {
				tags[tagTotal] = []string{parts[1]}
			}
		}
	}
	parseTuple("track")
	parseTuple("disc")

	// Adjust some ID3 tags
	parseLyrics(tags)
	parseTIPL(tags)
	delete(tags, "tmcl") // TMCL is already parsed by TagLib

	return &metadata.Info{
		Tags:            tags,
		AudioProperties: ap,
		HasPicture:      tags["has_picture"] != nil && len(tags["has_picture"]) > 0 && tags["has_picture"][0] == "true",
	}, nil
}

// parseLyrics make sure lyrics tags have language
func parseLyrics(tags map[string][]string) {
	lyrics := tags["lyrics"]
	if len(lyrics) > 0 {
		tags["lyrics:xxx"] = lyrics
		delete(tags, "lyrics")
	}
}

// These are the only roles we support, based on Picard's tag map:
// https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
var tiplMapping = map[string]string{
	"arranger": "arranger",
	"engineer": "engineer",
	"producer": "producer",
	"mix":      "mixer",
	"DJ-mix":   "djmixer",
}

// parseProp parses a property from the tags map and sets it to the target integer.
// It also deletes the property from the tags map after parsing.
func parseProp(tags map[string][]string, prop string) int {
	if value, ok := tags[prop]; ok && len(value) > 0 {
		v, _ := strconv.Atoi(value[0])
		delete(tags, prop)
		return v
	}
	return 0
}

// parseBasicTag checks if a basic tag (like __title, __artist, etc.) exists in the tags map.
// If it does, it moves the value to a more appropriate tag name (like title, artist, etc.),
// and deletes the basic tag from the map. If the target tag already exists, it ignores the basic tag.
func parseBasicTag(tags map[string][]string, basicName string, tagName string) {
	basicValue := tags[basicName]
	if len(basicValue) == 0 {
		return
	}
	delete(tags, basicName)
	if len(tags[tagName]) == 0 {
		tags[tagName] = basicValue
	}
}

// parseTIPL parses the ID3v2.4 TIPL frame string, which is received from TagLib in the format:
//
//	"arranger Andrew Powell engineer Chris Blair engineer Pat Stapley producer Eric Woolfson".
//
// and breaks it down into a map of roles and names, e.g.:
//
//	{"arranger": ["Andrew Powell"], "engineer": ["Chris Blair", "Pat Stapley"], "producer": ["Eric Woolfson"]}.
func parseTIPL(tags map[string][]string) {
	tipl := tags["tipl"]
	if len(tipl) == 0 {
		return
	}

	addRole := func(currentRole string, currentValue []string) {
		if currentRole != "" && len(currentValue) > 0 {
			role := tiplMapping[currentRole]
			tags[role] = append(tags[role], strings.Join(currentValue, " "))
		}
	}

	var currentRole string
	var currentValue []string
	for _, part := range strings.Split(tipl[0], " ") {
		if _, ok := tiplMapping[part]; ok {
			addRole(currentRole, currentValue)
			currentRole = part
			currentValue = nil
			continue
		}
		currentValue = append(currentValue, part)
	}
	addRole(currentRole, currentValue)
	delete(tags, "tipl")
}

var _ local.Extractor = (*extractor)(nil)

func init() {
	local.RegisterExtractor("taglib", func(_ fs.FS, baseDir string) local.Extractor {
		// ignores fs, as taglib extractor only works with local files
		return &extractor{baseDir}
	})
	conf.AddHook(func() {
		log.Debug("TagLib version", "version", Version())
	})
}
