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{}
	if length, ok := tags["_lengthinmilliseconds"]; ok && len(length) > 0 {
		millis, _ := strconv.Atoi(length[0])
		if millis > 0 {
			ap.Duration = (time.Millisecond * time.Duration(millis)).Round(time.Millisecond * 10)
		}
		delete(tags, "_lengthinmilliseconds")
	}
	parseProp := func(prop string, target *int) {
		if value, ok := tags[prop]; ok && len(value) > 0 {
			*target, _ = strconv.Atoi(value[0])
			delete(tags, prop)
		}
	}
	parseProp("_bitrate", &ap.BitRate)
	parseProp("_channels", &ap.Channels)
	parseProp("_samplerate", &ap.SampleRate)
	parseProp("_bitspersample", &ap.BitDepth)

	// 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",
}

// 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())
	})
}
