package chromium

import (
	"io/fs"
	"os"
	"path/filepath"
	"strings"

	"github.com/moond4rk/hackbrowserdata/browserdata"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/types"
	"github.com/moond4rk/hackbrowserdata/utils/fileutil"
	"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)

type Chromium struct {
	name        string
	storage     string
	profilePath string
	masterKey   []byte
	dataTypes   []types.DataType
	Paths       map[types.DataType]string
}

// New create instance of Chromium browser, fill item's path if item is existed.
func New(name, storage, profilePath string, dataTypes []types.DataType) ([]*Chromium, error) {
	c := &Chromium{
		name:        name,
		storage:     storage,
		profilePath: profilePath,
		dataTypes:   dataTypes,
	}
	multiDataTypePaths, err := c.userDataTypePaths(c.profilePath, c.dataTypes)
	if err != nil {
		return nil, err
	}
	chromiumList := make([]*Chromium, 0, len(multiDataTypePaths))
	for user, itemPaths := range multiDataTypePaths {
		chromiumList = append(chromiumList, &Chromium{
			name:      fileutil.BrowserName(name, user),
			dataTypes: typeutil.Keys(itemPaths),
			Paths:     itemPaths,
			storage:   storage,
		})
	}
	return chromiumList, nil
}

func (c *Chromium) Name() string {
	return c.name
}

func (c *Chromium) BrowsingData(isFullExport bool) (*browserdata.BrowserData, error) {
	// delete chromiumKey from dataTypes, doesn't need to export key
	var dataTypes []types.DataType
	for _, dt := range c.dataTypes {
		if dt != types.ChromiumKey {
			dataTypes = append(dataTypes, dt)
		}
	}

	if !isFullExport {
		dataTypes = types.FilterSensitiveItems(c.dataTypes)
	}

	data := browserdata.New(dataTypes)

	if err := c.copyItemToLocal(); err != nil {
		return nil, err
	}

	masterKey, err := c.GetMasterKey()
	if err != nil {
		return nil, err
	}

	c.masterKey = masterKey
	if err := data.Recovery(c.masterKey); err != nil {
		return nil, err
	}

	return data, nil
}

func (c *Chromium) copyItemToLocal() error {
	for i, path := range c.Paths {
		filename := i.TempFilename()
		var err error
		switch {
		case fileutil.IsDirExists(path):
			if i == types.ChromiumLocalStorage {
				err = fileutil.CopyDir(path, filename, "lock")
			}
			if i == types.ChromiumSessionStorage {
				err = fileutil.CopyDir(path, filename, "lock")
			}
		default:
			err = fileutil.CopyFile(path, filename)
		}
		if err != nil {
			log.Errorf("copy item to local, path %s, filename %s err %v", path, filename, err)
			continue
		}
	}
	return nil
}

// userDataTypePaths return a map of user to item path, map[profile 1][item's name & path key pair]
func (c *Chromium) userDataTypePaths(profilePath string, items []types.DataType) (map[string]map[types.DataType]string, error) {
	multiItemPaths := make(map[string]map[types.DataType]string)
	parentDir := fileutil.ParentDir(profilePath)
	err := filepath.Walk(parentDir, chromiumWalkFunc(items, multiItemPaths))
	if err != nil {
		return nil, err
	}
	var keyPath string
	var dir string
	for userDir, profiles := range multiItemPaths {
		for _, profile := range profiles {
			if strings.HasSuffix(profile, types.ChromiumKey.Filename()) {
				keyPath = profile
				dir = userDir
				break
			}
		}
	}
	t := make(map[string]map[types.DataType]string)
	for userDir, v := range multiItemPaths {
		if userDir == dir {
			continue
		}
		t[userDir] = v
		t[userDir][types.ChromiumKey] = keyPath
		fillLocalStoragePath(t[userDir], types.ChromiumLocalStorage)
	}
	return t, nil
}

// chromiumWalkFunc return a filepath.WalkFunc to find item's path
func chromiumWalkFunc(items []types.DataType, multiItemPaths map[string]map[types.DataType]string) filepath.WalkFunc {
	return func(path string, info fs.FileInfo, err error) error {
		if err != nil {
			if os.IsPermission(err) {
				log.Warnf("skipping walk chromium path permission error, path %s, err %v", path, err)
				return nil
			}
			return err
		}
		for _, v := range items {
			if info.Name() != v.Filename() {
				continue
			}
			if strings.Contains(path, "System Profile") {
				continue
			}
			if strings.Contains(path, "Snapshot") {
				continue
			}
			if strings.Contains(path, "def") {
				continue
			}
			profileFolder := fileutil.ParentBaseDir(path)
			if strings.Contains(filepath.ToSlash(path), "/Network/Cookies") {
				profileFolder = fileutil.BaseDir(strings.ReplaceAll(filepath.ToSlash(path), "/Network/Cookies", ""))
			}
			if _, exist := multiItemPaths[profileFolder]; exist {
				multiItemPaths[profileFolder][v] = path
			} else {
				multiItemPaths[profileFolder] = map[types.DataType]string{v: path}
			}
		}
		return nil
	}
}

func fillLocalStoragePath(itemPaths map[types.DataType]string, storage types.DataType) {
	if p, ok := itemPaths[types.ChromiumHistory]; ok {
		lsp := filepath.Join(filepath.Dir(p), storage.Filename())
		if fileutil.IsDirExists(lsp) {
			itemPaths[types.ChromiumLocalStorage] = lsp
		}
	}
}
