package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"path"
	"regexp"
	"strings"
	"text/template"
	"time"

	"slices"

	"github.com/dhowden/tag"
	"github.com/joho/godotenv"
)

type DownloadDirectoryComplete struct {
	Version             int    `json:"version"`
	LocalDirectoryName  string `json:"localDirectoryName"`
	RemoteDirectoryName string `json:"remoteDirectoryName"`
	Username            string `json:"username"`
}

var printLogs = true

func (d *DownloadDirectoryComplete) WarnVersion() {
	supportedVersions := []int{ 0 }
	if !slices.Contains(supportedVersions, d.Version) {
		supportedVersionsStr := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(supportedVersions)), ","), "[]")
		log.Printf("WARNING: Given data version not supported. Supported versions: %s. Given version: %d. Continuing, but there may be demons\n", supportedVersionsStr, d.Version)
	}
}

func main() {
	err := mainErr()
	if err != nil {
		log.Fatal("ERROR: ", err)
	}
}

func mainErr() error {
	if _, err := os.Stat(".env"); err == nil {
		if err := godotenv.Load(".env"); err != nil {
			return err
		}
	}

	execPath, _ := path.Split(os.Args[0])
	localConfPath := path.Join(execPath, "music-rename.env")
	if _, err := os.Stat(localConfPath); err == nil {
		if err := godotenv.Load(localConfPath); err != nil {
			return err
		}
	}
	
	directory, err := getDir()
	if err != nil {
		return err
	}

	return mainPath(directory)
}

func getDir() (string, error) {
	if len(os.Args) > 1 {
		return os.Args[1], nil
	} else {
		dataArg, found := os.LookupEnv("SLSKD_SCRIPT_DATA")
		if !found {
			return "", errors.New("missing any reference to a path")
		}
		var data DownloadDirectoryComplete
		if err := json.Unmarshal([]byte(dataArg), &data); err != nil {
			return "", err
		}

		data.WarnVersion()
		printLogs = false
		
		return data.LocalDirectoryName, nil
	}
}

func mainPath(directory string) error {

	if err := testEnv(); err != nil {
		return err
	}

	files, err := os.ReadDir(directory)
	if err != nil {
		return err
	}
	if len(files) == 0 {
		return errors.New("no files found")
	}

	// read tags
	tags, _ := getTagsInDir(directory)

	// move files
	if tags != nil {
		if printLogs {
			log.Println("found tags, moving based on tags")
		}
		err := doMoveTags(directory, tags)
		if err != nil {
			return err
		}
	} else {
		if printLogs {
			log.Println("no tags found, moving to notags folder")
		}
		err := doMoveNotags(directory)
		if err != nil {
			return err
		}
	}

	return nil
}

func testEnv() error {
	_, ok := os.LookupEnv("LIBRARY_ROOT")
	if !ok {
		return errors.New("missing LIBRARY_ROOT")
	}

	_, ok = os.LookupEnv("LIBRARY_FILETEMPLATE_PATH")
	if !ok {
		return errors.New("missing LIBRARY_FILETEMPLATE_PATH")
	}

	_, ok = os.LookupEnv("LIBRARY_FILETEMPLATE_FILE")
	if !ok {
		return errors.New("missing LIBRARY_FILETEMPLATE_FILE")
	}

	_, ok = os.LookupEnv("LIBRARY_NOTAGS_ROOT")
	if !ok {
		return errors.New("missing LIBRARY_NOTAGS_ROOT")
	}

	return nil
}

func getTagsInDir(directory string) (tag.Metadata, error) {
	files, err := os.ReadDir(directory)
	if err != nil {
		return nil, err
	}

	missingTags := false
	for _, file := range files {
		fname := path.Join(directory, file.Name())

		if file.IsDir() {
			continue
		}

		file, err := os.Open(fname)
		if err != nil {
			continue
		}
		defer file.Close()

		tags, err := tag.ReadFrom(file)
		if err != nil {
			continue
		}

		// if we found tags but they're incomplete, switch the flag
		if !checkTags(tags) {
			missingTags = true
			continue
		}

		// found tag data
		return tags, nil
	}
	
	// no tag data in any file
	if missingTags {
		return nil, errors.New("files have incomplete tag data")
	} else {
		return nil, errors.New("unable to read tag data")
	}
}

func checkTags(tags tag.Metadata) bool {
	// most important tags
	if tags.Album() == "" || tags.AlbumArtist() == "" || tags.Artist() == "" || tags.Title() == "" {
		return false
	}
	return true
}

func doMoveNotags(directory string) error {
	newRoot := os.Getenv("LIBRARY_NOTAGS_ROOT")
	_, folderName := path.Split(directory)
	toDirectory := path.Join(newRoot, folderName)

	return move(directory, toDirectory)
}

func doMoveTags(directory string, albumTags tag.Metadata) error {
	// move all tagged files
	files, err := os.ReadDir(directory)
	if err != nil {
		return err
	}

	for _, file := range files {
		if file.IsDir() {
			continue
		}

		if file.Name()[0] == '.' {
			continue
		}

		fname := path.Join(directory, file.Name())
		file, err := os.Open(fname)
		if err != nil {
			continue
		}
		defer file.Close()

		tags, err := tag.ReadFrom(file)
		if err != nil {
			continue
		}

		err = doMoveTagged(fname, tags)
		if err != nil {
			return err
		}
	}

	// move the rest
	files, err = os.ReadDir(directory)
	if err != nil {
		return err
	}

	for _, file := range files {
		if file.Name()[0] == '.' {
			continue
		}

		fname := path.Join(directory, file.Name())

		err = doMoveTaggedPathonly(fname, albumTags)
		if err != nil {
			return err
		}
	}

	// delete folder
	files, err = os.ReadDir(directory)
	if err != nil {
		return err
	}

	if len(files) == 0 {
		return os.Remove(directory)
	}

	// some files remain, wait a second to see if they go away
	time.Sleep(1 * time.Second)
	files, err = os.ReadDir(directory)
	if err != nil {
		return err
	}

	if len(files) == 0 {
		return os.Remove(directory)
	}

	return nil
}

type LibTemplatePath struct {
	Album string
	AlbumArtist string
	Genre string
	Year int
}

type LibTemplateFile struct {
	Title string
	Album string
	Artist string
	AlbumArtist string
	Composer string
	Genre string
	Year int
	Track int
	TrackCount int
	Disc int
	DiscCount int
}

func doMoveTagged(filePath string, fileTags tag.Metadata) error {
	libRoot := os.Getenv("LIBRARY_ROOT")
	libTemplatePath := os.Getenv("LIBRARY_FILETEMPLATE_PATH")
	libTemplateFile := os.Getenv("LIBRARY_FILETEMPLATE_FILE")
	fileExt := path.Ext(filePath)

	track, trackCount := fileTags.Track()
	disc, discCount := fileTags.Disc()

	tmplPath := template.Must(template.New("libPath").Parse(libTemplatePath))
	tmplFile := template.Must(template.New("libFile").Parse(libTemplateFile))

	tmplDataFile := LibTemplateFile{
		Title: fileTags.Title(),
		Album: fileTags.Album(),
		Artist: fileTags.Artist(),
		AlbumArtist: fileTags.AlbumArtist(),
		Composer: fileTags.Composer(),
		Genre: fileTags.Genre(),
		Year: fileTags.Year(),
		Track: track,
		TrackCount: trackCount,
		Disc: disc,
		DiscCount: discCount,
	}

	tmplDataPath := LibTemplatePath{
		Album: cleanTemplateVariable(fileTags.Album()),
		AlbumArtist: cleanTemplateVariable(fileTags.AlbumArtist()),
		Genre: cleanTemplateVariable(fileTags.Genre()),
		Year: fileTags.Year(),
	}

	bufPath := bytes.NewBufferString("")
	err := tmplPath.Execute(bufPath, tmplDataPath)
	if err != nil {
		return err
	}

	bufFile := bytes.NewBufferString("")
	err = tmplFile.Execute(bufFile, tmplDataFile)
	if err != nil {
		return err
	}

	libFile := path.Join(libRoot, bufPath.String(), bufFile.String() + fileExt)

	return move(filePath, libFile)
}

func doMoveTaggedPathonly(filePath string, albumTags tag.Metadata) error {
	libRoot := os.Getenv("LIBRARY_ROOT")
	libTemplatePath := os.Getenv("LIBRARY_FILETEMPLATE_PATH")
	_, fileName := path.Split(filePath)

	tmplPath := template.Must(template.New("libPath").Parse(libTemplatePath))

	tmplData := LibTemplatePath{
		Album: cleanTemplateVariable(albumTags.Album()),
		AlbumArtist: cleanTemplateVariable(albumTags.AlbumArtist()),
		Genre: cleanTemplateVariable(albumTags.Genre()),
		Year: albumTags.Year(),
	}

	bufPath := bytes.NewBufferString("")
	err := tmplPath.Execute(bufPath, tmplData)
	if err != nil {
		return err
	}

	libFile := path.Join(libRoot, bufPath.String(), fileName)

	return move(filePath, libFile)
}

func ensureDirectoryPath(filePath string) error {
	directoryPath, _ := path.Split(filePath)
	return os.MkdirAll(directoryPath, 0777)
}

func move(from, to string) error {
	to = cleanPath(to)
	if _, err := os.Stat(to); err == nil {
		return fmt.Errorf("file already exists: %s", to)
	}

	if err := ensureDirectoryPath(to); err != nil {
		return err
	}

	if printLogs {
		log.Printf("moving \"%s\" => \"%s\"\n", from, to)
	}
	// os.Rename gives error "invalid cross-device link"
	if err := os.Rename(from, to); err != nil {
		return moveFile(from, to)
	}
	return nil
}

func moveFile(source, destination string) (err error) {
    src, err := os.Open(source)
    if err != nil {
        return err
    }
    defer src.Close()
    fi, err := src.Stat()
    if err != nil {
        return err
    }
    flag := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
    perm := fi.Mode() & os.ModePerm
    dst, err := os.OpenFile(destination, flag, perm)
    if err != nil {
        return err
    }
    defer dst.Close()
    _, err = io.Copy(dst, src)
    if err != nil {
        dst.Close()
        os.Remove(destination)
        return err
    }
    err = dst.Close()
    if err != nil {
        return err
    }
    err = src.Close()
    if err != nil {
        return err
    }
    err = os.Remove(source)
    if err != nil {
        return err
    }
    return nil
}

func cleanPath(str string) string {
	re := regexp.MustCompile(`[<>:"|?*]`)
	return re.ReplaceAllString(str, "")
}

func cleanTemplateVariable(str string) string {
	re := regexp.MustCompile(`[<>:"|?*/\\]`)
	return re.ReplaceAllString(str, "")
}
