package scanner

import (
	"bufio"
	"errors"
	"mime"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/deluan/navidrome/conf"
	"github.com/deluan/navidrome/log"
)

type Metadata struct {
	filePath string
	suffix   string
	fileInfo os.FileInfo
	tags     map[string]string
}

func (m *Metadata) Title() string               { return m.tags["title"] }
func (m *Metadata) Album() string               { return m.tags["album"] }
func (m *Metadata) Artist() string              { return m.tags["artist"] }
func (m *Metadata) AlbumArtist() string         { return m.tags["album_artist"] }
func (m *Metadata) Composer() string            { return m.tags["composer"] }
func (m *Metadata) Genre() string               { return m.tags["genre"] }
func (m *Metadata) Year() int                   { return m.parseYear("year") }
func (m *Metadata) TrackNumber() (int, int)     { return m.parseTuple("trackNum", "trackTotal") }
func (m *Metadata) DiscNumber() (int, int)      { return m.parseTuple("discNum", "discTotal") }
func (m *Metadata) HasPicture() bool            { return m.tags["hasPicture"] == "Video" }
func (m *Metadata) Comment() string             { return m.tags["comment"] }
func (m *Metadata) Compilation() bool           { return m.parseBool("compilation") }
func (m *Metadata) Duration() int               { return m.parseDuration("duration") }
func (m *Metadata) BitRate() int                { return m.parseInt("bitrate") }
func (m *Metadata) ModificationTime() time.Time { return m.fileInfo.ModTime() }
func (m *Metadata) FilePath() string            { return m.filePath }
func (m *Metadata) Suffix() string              { return m.suffix }
func (m *Metadata) Size() int                   { return int(m.fileInfo.Size()) }

func ExtractAllMetadata(dirPath string) (map[string]*Metadata, error) {
	dir, err := os.Open(dirPath)
	if err != nil {
		return nil, err
	}
	files, err := dir.Readdir(-1)
	if err != nil {
		return nil, err
	}
	var audioFiles []string
	for _, f := range files {
		if f.IsDir() {
			continue
		}
		filePath := filepath.Join(dirPath, f.Name())
		extension := path.Ext(filePath)
		if !isAudioFile(extension) {
			continue
		}
		audioFiles = append(audioFiles, filePath)
	}

	if len(audioFiles) == 0 {
		return map[string]*Metadata{}, nil
	}
	return probe(audioFiles)
}

func probe(inputs []string) (map[string]*Metadata, error) {
	cmdLine, args := createProbeCommand(inputs)

	log.Trace("Executing command", "arg0", cmdLine, "args", args)
	cmd := exec.Command(cmdLine, args...)
	output, _ := cmd.CombinedOutput()
	mds := map[string]*Metadata{}
	if len(output) == 0 {
		return mds, errors.New("error extracting metadata files")
	}
	infos := parseOutput(string(output))
	for file, info := range infos {
		md, err := extractMetadata(file, info)
		// Skip files with errors
		if err == nil {
			mds[file] = md
		}
	}
	return mds, nil
}

var inputRegex = regexp.MustCompile(`(?m)^Input #\d+,.*,\sfrom\s'(.*)'`)

func parseOutput(output string) map[string]string {
	split := map[string]string{}
	all := inputRegex.FindAllStringSubmatchIndex(output, -1)
	for i, loc := range all {
		// Filename is the first captured group
		file := output[loc[2]:loc[3]]

		// File info is everything from the match, up until the beginning of the next match
		info := ""
		initial := loc[1]
		if i < len(all)-1 {
			end := all[i+1][0] - 1
			info = output[initial:end]
		} else {
			// if this is the last match
			info = output[initial:]
		}
		split[file] = info
	}
	return split
}

func extractMetadata(filePath, info string) (*Metadata, error) {
	m := &Metadata{filePath: filePath, tags: map[string]string{}}
	m.suffix = strings.ToLower(strings.TrimPrefix(path.Ext(filePath), "."))
	var err error
	m.fileInfo, err = os.Stat(filePath)
	if err != nil {
		log.Warn("Error stating file. Skipping", "filePath", filePath, err)
		return nil, errors.New("error stating file")
	}

	m.parseInfo(info)
	if len(m.tags) == 0 {
		log.Trace("Not a media file. Skipping", "filePath", filePath)
		return nil, errors.New("not a media file")
	}
	return m, nil
}

func isAudioFile(extension string) bool {
	typ := mime.TypeByExtension(extension)
	return strings.HasPrefix(typ, "audio/")
}

var (
	tagsRx = map[*regexp.Regexp]string{
		regexp.MustCompile(`(?i)^\s{4}compilation\s+:(.*)`):    "compilation",
		regexp.MustCompile(`(?i)^\s{4}genre\s+:\s(.*)`):        "genre",
		regexp.MustCompile(`(?i)^\s{4}title\s+:\s(.*)`):        "title",
		regexp.MustCompile(`(?i)^\s{4}comment\s+:\s(.*)`):      "comment",
		regexp.MustCompile(`(?i)^\s{4}artist\s+:\s(.*)`):       "artist",
		regexp.MustCompile(`(?i)^\s{4}album_artist\s+:\s(.*)`): "album_artist",
		regexp.MustCompile(`(?i)^\s{4}TCM\s+:\s(.*)`):          "composer",
		regexp.MustCompile(`(?i)^\s{4}album\s+:\s(.*)`):        "album",
		regexp.MustCompile(`(?i)^\s{4}track\s+:\s(.*)`):        "trackNum",
		regexp.MustCompile(`(?i)^\s{4}tracktotal\s+:\s(.*)`):   "trackTotal",
		regexp.MustCompile(`(?i)^\s{4}disc\s+:\s(.*)`):         "discNum",
		regexp.MustCompile(`(?i)^\s{4}disctotal\s+:\s(.*)`):    "discTotal",
		regexp.MustCompile(`(?i)^\s{4}TPA\s+:\s(.*)`):          "discNum",
		regexp.MustCompile(`(?i)^\s{4}date\s+:\s(.*)`):         "year",
		regexp.MustCompile(`^\s{4}Stream #\d+:\d+: (.+):\s`):   "hasPicture",
	}

	durationRx = regexp.MustCompile(`^\s\sDuration: ([\d.:]+).*bitrate: (\d+)`)
)

func (m *Metadata) parseInfo(info string) {
	reader := strings.NewReader(info)
	scanner := bufio.NewScanner(reader)
	for scanner.Scan() {
		line := scanner.Text()
		for rx, tag := range tagsRx {
			match := rx.FindStringSubmatch(line)
			if len(match) > 0 {
				m.tags[tag] = match[1]
				break
			}
			match = durationRx.FindStringSubmatch(line)
			if len(match) == 0 {
				continue
			}
			m.tags["duration"] = match[1]
			if len(match) > 1 {
				m.tags["bitrate"] = match[2]
			}
		}
	}
}

func (m *Metadata) parseInt(tagName string) int {
	if v, ok := m.tags[tagName]; ok {
		i, _ := strconv.Atoi(v)
		return i
	}
	return 0
}

var tagYearFormats = []string{
	"2006",
	"2006.01",
	"2006.01.02",
	"2006-01",
	"2006-01-02",
	time.RFC3339,
}

func (m *Metadata) parseYear(tagName string) int {
	if v, ok := m.tags[tagName]; ok {
		var y time.Time
		var err error
		for _, fmt := range tagYearFormats {
			if y, err = time.Parse(fmt, v); err == nil {
				break
			}
		}
		if err != nil {
			log.Error("Error parsing year from ffmpeg date field. Please report this issue", "file", m.filePath, "date", v)
			return 0
		}
		return y.Year()
	}
	return 0
}

func (m *Metadata) parseTuple(numTag string, totalTag string) (int, int) {
	if v, ok := m.tags[numTag]; ok {
		tuple := strings.Split(v, "/")
		t1, t2 := 0, 0
		t1, _ = strconv.Atoi(tuple[0])
		if len(tuple) > 1 {
			t2, _ = strconv.Atoi(tuple[1])
		} else {
			t2, _ = strconv.Atoi(m.tags[totalTag])
		}
		return t1, t2
	}
	return 0, 0
}

func (m *Metadata) parseBool(tagName string) bool {
	if v, ok := m.tags[tagName]; ok {
		i, _ := strconv.Atoi(strings.TrimSpace(v))
		return i == 1
	}
	return false
}

var zeroTime = time.Date(0000, time.January, 1, 0, 0, 0, 0, time.UTC)

func (m *Metadata) parseDuration(tagName string) int {
	if v, ok := m.tags[tagName]; ok {
		d, err := time.Parse("15:04:05", v)
		if err != nil {
			return 0
		}
		return int(d.Sub(zeroTime).Seconds())
	}
	return 0
}

func createProbeCommand(inputs []string) (string, []string) {
	cmd := conf.Server.ProbeCommand

	split := strings.Split(cmd, " ")
	args := make([]string, 0)
	first := true
	for _, s := range split {
		if s == "%s" {
			for _, inp := range inputs {
				if !first {
					args = append(args, "-i")
				}
				args = append(args, inp)
				first = false
			}
			continue
		}
		args = append(args, s)
	}

	return args[0], args[1:]
}
