package main

import (
	"encoding/json"
	"io/fs"
	"log"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

type Storage struct {
	timePattern                                                                                            string
	invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path string
	invites                                                                                                Invites
	profiles                                                                                               map[string]Profile
	defaultProfile                                                                                         string
	emails, policy, configuration, displayprefs, ombi_template                                             map[string]interface{}
	lang                                                                                                   Lang
}

// timePattern: %Y-%m-%dT%H:%M:%S.%f

type Profile struct {
	Admin         bool                   `json:"admin,omitempty"`
	LibraryAccess string                 `json:"libraries,omitempty"`
	FromUser      string                 `json:"fromUser,omitempty"`
	Policy        map[string]interface{} `json:"policy,omitempty"`
	Configuration map[string]interface{} `json:"configuration,omitempty"`
	Displayprefs  map[string]interface{} `json:"displayprefs,omitempty"`
	Default       bool                   `json:"default,omitempty"`
}

type Invite struct {
	Created       time.Time                  `json:"created"`
	NoLimit       bool                       `json:"no-limit"`
	RemainingUses int                        `json:"remaining-uses"`
	ValidTill     time.Time                  `json:"valid_till"`
	Email         string                     `json:"email"`
	UsedBy        [][]string                 `json:"used-by"`
	Notify        map[string]map[string]bool `json:"notify"`
	Profile       string                     `json:"profile"`
	Label         string                     `json:"label,omitempty"`
	Keys          []string                   `json"keys,omitempty"`
}

type Lang struct {
	chosenFormLang  string
	chosenAdminLang string
	chosenEmailLang string
	AdminPath       string
	Admin           adminLangs
	AdminJSON       map[string]string
	FormPath        string
	Form            formLangs
	EmailPath       string
	Email           emailLangs
	CommonPath      string
	Common          commonLangs
	SetupPath       string
	Setup           setupLangs
}

func (st *Storage) loadLang(filesystems ...fs.FS) (err error) {
	err = st.loadLangCommon(filesystems...)
	if err != nil {
		return
	}
	err = st.loadLangAdmin(filesystems...)
	if err != nil {
		return
	}
	err = st.loadLangForm(filesystems...)
	if err != nil {
		return
	}
	err = st.loadLangEmail(filesystems...)
	return
}

func (common *commonLangs) patchCommon(lang string, other *langSection) {
	if *other == nil {
		*other = langSection{}
	}
	if _, ok := (*common)[lang]; !ok {
		lang = "en-us"
	}
	for n, ev := range (*common)[lang].Strings {
		if v, ok := (*other)[n]; !ok || v == "" {
			(*other)[n] = ev
		}
	}
}

// If a given language has missing values, fill it in with the english value.
func patchLang(english, other *langSection) {
	if *other == nil {
		*other = langSection{}
	}
	for n, ev := range *english {
		if v, ok := (*other)[n]; !ok || v == "" {
			(*other)[n] = ev
		}
	}
}

func patchQuantityStrings(english, other *map[string]quantityString) {
	for n, ev := range *english {
		qs, ok := (*other)[n]
		if !ok {
			(*other)[n] = ev
			return
		} else if qs.Singular == "" {
			qs.Singular = ev.Singular
		} else if (*other)[n].Plural == "" {
			qs.Plural = ev.Plural
		}
		(*other)[n] = qs
	}
}

func (st *Storage) loadLangCommon(filesystems ...fs.FS) error {
	st.lang.Common = map[string]commonLang{}
	var english commonLang
	load := func(filesystem fs.FS, fname string) error {
		index := strings.TrimSuffix(fname, filepath.Ext(fname))
		lang := commonLang{}
		f, err := fs.ReadFile(filesystem, FSJoin(st.lang.CommonPath, fname))
		if err != nil {
			return err
		}
		if substituteStrings != "" {
			f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
		}
		err = json.Unmarshal(f, &lang)
		if err != nil {
			return err
		}
		if fname != "en-us.json" {
			patchLang(&english.Strings, &lang.Strings)
		}
		st.lang.Common[index] = lang
		return nil
	}
	engFound := false
	var err error
	for _, filesystem := range filesystems {
		err = load(filesystem, "en-us.json")
		if err == nil {
			engFound = true
		}
	}
	if !engFound {
		return err
	}
	english = st.lang.Common["en-us"]
	commonLoaded := false
	for _, filesystem := range filesystems {
		files, err := fs.ReadDir(filesystem, st.lang.CommonPath)
		if err != nil {
			continue
		}
		for _, f := range files {
			if f.Name() != "en-us.json" {
				err = load(filesystem, f.Name())
				if err == nil {
					commonLoaded = true
				}
			}
		}
	}
	if !commonLoaded {
		return err
	}
	return nil
}

func (st *Storage) loadLangAdmin(filesystems ...fs.FS) error {
	st.lang.Admin = map[string]adminLang{}
	var english adminLang
	load := func(filesystem fs.FS, fname string) error {
		index := strings.TrimSuffix(fname, filepath.Ext(fname))
		lang := adminLang{}
		f, err := fs.ReadFile(filesystem, FSJoin(st.lang.AdminPath, fname))
		if err != nil {
			return err
		}
		if substituteStrings != "" {
			f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
		}
		err = json.Unmarshal(f, &lang)
		if err != nil {
			return err
		}
		st.lang.Common.patchCommon(index, &lang.Strings)
		if fname != "en-us.json" {
			patchLang(&english.Strings, &lang.Strings)
			patchLang(&english.Notifications, &lang.Notifications)
			patchQuantityStrings(&english.QuantityStrings, &lang.QuantityStrings)
		}
		stringAdmin, err := json.Marshal(lang)
		if err != nil {
			return err
		}
		lang.JSON = string(stringAdmin)
		st.lang.Admin[index] = lang
		return nil
	}
	engFound := false
	var err error
	for _, filesystem := range filesystems {
		err = load(filesystem, "en-us.json")
		if err == nil {
			engFound = true
		}
	}
	if !engFound {
		return err
	}
	english = st.lang.Admin["en-us"]
	adminLoaded := false
	for _, filesystem := range filesystems {
		files, err := fs.ReadDir(filesystem, st.lang.AdminPath)
		if err != nil {
			continue
		}
		for _, f := range files {
			if f.Name() != "en-us.json" {
				err = load(filesystem, f.Name())
				if err == nil {
					adminLoaded = true
				}
			}
		}
	}
	if !adminLoaded {
		return err
	}
	return nil
}

func (st *Storage) loadLangForm(filesystems ...fs.FS) error {
	st.lang.Form = map[string]formLang{}
	var english formLang
	load := func(filesystem fs.FS, fname string) error {
		index := strings.TrimSuffix(fname, filepath.Ext(fname))
		lang := formLang{}
		f, err := fs.ReadFile(filesystem, FSJoin(st.lang.FormPath, fname))
		if err != nil {
			return err
		}
		if substituteStrings != "" {
			f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
		}
		err = json.Unmarshal(f, &lang)
		if err != nil {
			return err
		}
		st.lang.Common.patchCommon(index, &lang.Strings)
		if fname != "en-us.json" {
			patchLang(&english.Strings, &lang.Strings)
			patchLang(&english.Notifications, &lang.Notifications)
			patchQuantityStrings(&english.ValidationStrings, &lang.ValidationStrings)
		}
		notifications, err := json.Marshal(lang.Notifications)
		if err != nil {
			return err
		}
		validationStrings, err := json.Marshal(lang.ValidationStrings)
		if err != nil {
			return err
		}
		lang.notificationsJSON = string(notifications)
		lang.validationStringsJSON = string(validationStrings)
		st.lang.Form[index] = lang
		return nil
	}
	engFound := false
	var err error
	for _, filesystem := range filesystems {
		err = load(filesystem, "en-us.json")
		if err == nil {
			engFound = true
		}
	}
	if !engFound {
		return err
	}
	english = st.lang.Form["en-us"]
	formLoaded := false
	for _, filesystem := range filesystems {
		files, err := fs.ReadDir(filesystem, st.lang.FormPath)
		if err != nil {
			continue
		}
		for _, f := range files {
			if f.Name() != "en-us.json" {
				err = load(filesystem, f.Name())
				if err == nil {
					formLoaded = true
				}
			}
		}
	}
	if !formLoaded {
		return err
	}
	return nil
}

func (st *Storage) loadLangEmail(filesystems ...fs.FS) error {
	st.lang.Email = map[string]emailLang{}
	var english emailLang
	load := func(filesystem fs.FS, fname string) error {
		index := strings.TrimSuffix(fname, filepath.Ext(fname))
		lang := emailLang{}
		f, err := fs.ReadFile(filesystem, FSJoin(st.lang.EmailPath, fname))
		if err != nil {
			return err
		}
		if substituteStrings != "" {
			f = []byte(strings.ReplaceAll(string(f), "Jellyfin", substituteStrings))
		}
		err = json.Unmarshal(f, &lang)
		if err != nil {
			return err
		}
		st.lang.Common.patchCommon(index, &lang.Strings)
		if fname != "en-us.json" {
			patchLang(&english.UserCreated, &lang.UserCreated)
			patchLang(&english.InviteExpiry, &lang.InviteExpiry)
			patchLang(&english.PasswordReset, &lang.PasswordReset)
			patchLang(&english.UserDeleted, &lang.UserDeleted)
			patchLang(&english.InviteEmail, &lang.InviteEmail)
			patchLang(&english.WelcomeEmail, &lang.WelcomeEmail)
		}
		st.lang.Email[index] = lang
		return nil
	}
	engFound := false
	var err error
	for _, filesystem := range filesystems {
		err = load(filesystem, "en-us.json")
		if err == nil {
			engFound = true
		}
	}
	if !engFound {
		return err
	}
	english = st.lang.Email["en-us"]
	emailLoaded := false
	for _, filesystem := range filesystems {
		files, err := fs.ReadDir(filesystem, st.lang.EmailPath)
		if err != nil {
			continue
		}
		for _, f := range files {
			if f.Name() != "en-us.json" {
				err = load(filesystem, f.Name())
				if err == nil {
					emailLoaded = true
				}
			}
		}
	}
	if !emailLoaded {
		return err
	}
	return nil
}

type Invites map[string]Invite

func (st *Storage) loadInvites() error {
	return loadJSON(st.invite_path, &st.invites)
}

func (st *Storage) storeInvites() error {
	return storeJSON(st.invite_path, st.invites)
}

func (st *Storage) loadEmails() error {
	return loadJSON(st.emails_path, &st.emails)
}

func (st *Storage) storeEmails() error {
	return storeJSON(st.emails_path, st.emails)
}

func (st *Storage) loadPolicy() error {
	return loadJSON(st.policy_path, &st.policy)
}

func (st *Storage) storePolicy() error {
	return storeJSON(st.policy_path, st.policy)
}

func (st *Storage) loadConfiguration() error {
	return loadJSON(st.configuration_path, &st.configuration)
}

func (st *Storage) storeConfiguration() error {
	return storeJSON(st.configuration_path, st.configuration)
}

func (st *Storage) loadDisplayprefs() error {
	return loadJSON(st.displayprefs_path, &st.displayprefs)
}

func (st *Storage) storeDisplayprefs() error {
	return storeJSON(st.displayprefs_path, st.displayprefs)
}

func (st *Storage) loadOmbiTemplate() error {
	return loadJSON(st.ombi_path, &st.ombi_template)
}

func (st *Storage) storeOmbiTemplate() error {
	return storeJSON(st.ombi_path, st.ombi_template)
}

func (st *Storage) loadProfiles() error {
	err := loadJSON(st.profiles_path, &st.profiles)
	for name, profile := range st.profiles {
		if profile.Default {
			st.defaultProfile = name
		}
		change := false
		if profile.Policy["IsAdministrator"] != nil {
			profile.Admin = profile.Policy["IsAdministrator"].(bool)
			change = true
		}
		if profile.Policy["EnabledFolders"] != nil {
			length := len(profile.Policy["EnabledFolders"].([]interface{}))
			if length == 0 {
				profile.LibraryAccess = "All"
			} else {
				profile.LibraryAccess = strconv.Itoa(length)
			}
			change = true
		}
		if profile.FromUser == "" {
			profile.FromUser = "Unknown"
			change = true
		}
		if change {
			st.profiles[name] = profile
		}
	}
	if st.defaultProfile == "" {
		for n := range st.profiles {
			st.defaultProfile = n
		}
	}
	return err
}

func (st *Storage) storeProfiles() error {
	return storeJSON(st.profiles_path, st.profiles)
}

func (st *Storage) migrateToProfile() error {
	st.loadPolicy()
	st.loadConfiguration()
	st.loadDisplayprefs()
	st.loadProfiles()
	st.profiles["Default"] = Profile{
		Policy:        st.policy,
		Configuration: st.configuration,
		Displayprefs:  st.displayprefs,
	}
	return st.storeProfiles()
}

func loadJSON(path string, obj interface{}) error {
	var file []byte
	var err error
	file, err = os.ReadFile(path)
	if err != nil {
		file = []byte("{}")
	}
	err = json.Unmarshal(file, &obj)
	if err != nil {
		log.Printf("ERROR: Failed to read \"%s\": %s", path, err)
	}
	return err
}

func storeJSON(path string, obj interface{}) error {
	data, err := json.Marshal(obj)
	if err != nil {
		return err
	}
	err = os.WriteFile(path, data, 0644)
	if err != nil {
		log.Printf("ERROR: Failed to write to \"%s\": %s", path, err)
	}
	return err
}

// One build of JF 10.7.0 hyphenated user IDs while another one later didn't. These functions will hyphenate/de-hyphenate email storage.

func hyphenate(userID string) string {
	if userID[8] == '-' {
		return userID
	}
	return userID[:8] + "-" + userID[8:12] + "-" + userID[12:16] + "-" + userID[16:20] + "-" + userID[20:]
}

func (app *appContext) deHyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
	jfUsers, status, err := app.jf.GetUsers(false)
	if status != 200 || err != nil {
		return nil, status, err
	}
	newEmails := map[string]interface{}{}
	for _, user := range jfUsers {
		unHyphenated := user["Id"].(string)
		hyphenated := hyphenate(unHyphenated)
		email, ok := old[hyphenated]
		if ok {
			newEmails[unHyphenated] = email
		}
	}
	return newEmails, status, err
}

func (app *appContext) hyphenateEmailStorage(old map[string]interface{}) (map[string]interface{}, int, error) {
	jfUsers, status, err := app.jf.GetUsers(false)
	if status != 200 || err != nil {
		return nil, status, err
	}
	newEmails := map[string]interface{}{}
	for _, user := range jfUsers {
		unstripped := user["Id"].(string)
		stripped := strings.ReplaceAll(unstripped, "-", "")
		email, ok := old[stripped]
		if ok {
			newEmails[unstripped] = email
		}
	}
	return newEmails, status, err
}