package cli

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"

	"github.com/zk-org/zk/internal/adapter/editor"
	"github.com/zk-org/zk/internal/adapter/fs"
	"github.com/zk-org/zk/internal/adapter/fzf"
	"github.com/zk-org/zk/internal/adapter/handlebars"
	hbhelpers "github.com/zk-org/zk/internal/adapter/handlebars/helpers"
	"github.com/zk-org/zk/internal/adapter/markdown"
	"github.com/zk-org/zk/internal/adapter/sqlite"
	"github.com/zk-org/zk/internal/adapter/term"
	"github.com/zk-org/zk/internal/core"
	"github.com/zk-org/zk/internal/util"
	executil "github.com/zk-org/zk/internal/util/exec"
	osutil "github.com/zk-org/zk/internal/util/os"
	"github.com/zk-org/zk/internal/util/pager"
	"github.com/zk-org/zk/internal/util/paths"
	"github.com/zk-org/zk/internal/util/rand"
)

type Dirs struct {
	NotebookDir string
	WorkingDir  string
}

type Container struct {
	Version            string
	Config             core.Config
	Logger             *util.ProxyLogger
	Styler             *core.ProxyStyler
	Terminal           *term.Terminal
	FS                 *fs.FileStorage
	TemplateLoader     core.TemplateLoader
	WorkingDir         string
	Notebooks          *core.NotebookStore
	currentNotebook    *core.Notebook
	currentNotebookErr error
}

func NewContainer(version string) (*Container, error) {
	term := term.New()
	styler := core.NewProxyStyler(term)
	logger := util.NewProxyLogger(util.NewStdLogger("zk: ", 0))
	fs, err := fs.NewFileStorage("", logger)
	if err != nil {
		return nil, err
	}
	config := core.NewDefaultConfig()

	handlebars.Init(term.SupportsUTF8(), logger)
	// Template loader used for embedded templates (e.g. default config, fzf
	// line, etc.).
	templateLoader := handlebars.NewLoader(handlebars.LoaderOpts{
		LookupPaths: []string{},
		Styler:      styler,
	})
	templateLoader.RegisterHelper("style", hbhelpers.NewStyleHelper(styler, logger))

	// Load global user config
	configPath, err := locateGlobalConfig()
	if err != nil {
		return nil, fmt.Errorf("locating config failed: %w", err)
	}
	if configPath != "" {
		config, err = core.OpenConfig(configPath, config, fs, true)
		if err != nil {
			return nil, fmt.Errorf("opening config failed: %w", err)
		}
	}

	// Set the default notebook if not already set
	// might be overrided if --notebook-dir flag is present
	if osutil.GetOptEnv("ZK_NOTEBOOK_DIR").IsNull() && !config.Notebook.Dir.IsNull() {
		notebookDir, err := paths.ExpandPath(config.Notebook.Dir.Unwrap())
		if err != nil {
			return nil, fmt.Errorf("expanding notebook dir failed: %w", err)
		}
		os.Setenv("ZK_NOTEBOOK_DIR", notebookDir)
	}

	// Register the shell helper with the resolved shell.
	shell := executil.ResolveShell(config.Tool.Shell)
	templateLoader.RegisterHelper("sh", hbhelpers.NewShellHelper(logger, shell))

	return &Container{
		Version:        version,
		Config:         config,
		Logger:         logger,
		Styler:         styler,
		Terminal:       term,
		FS:             fs,
		TemplateLoader: templateLoader,
		Notebooks: core.NewNotebookStore(config, core.NotebookStorePorts{
			FS:             fs,
			TemplateLoader: templateLoader,
			NotebookFactory: func(path string, config core.Config) (*core.Notebook, error) {
				dbPath := filepath.Join(path, ".zk/notebook.db")
				db, err := sqlite.Open(dbPath)
				if err != nil {
					return nil, err
				}

				notebook := core.NewNotebook(path, config, core.NotebookPorts{
					NoteIndex: sqlite.NewNoteIndex(path, db, logger, config.Note.Extension),
					NoteContentParser: markdown.NewParser(
						markdown.ParserOpts{
							HashtagEnabled:      config.Format.Markdown.Hashtags,
							MultiWordTagEnabled: config.Format.Markdown.MultiwordTags,
							ColontagEnabled:     config.Format.Markdown.ColonTags,
						},
						logger,
					),
					TemplateLoaderFactory: func(language string) (core.TemplateLoader, error) {
						loader := handlebars.NewLoader(handlebars.LoaderOpts{
							LookupPaths: []string{
								filepath.Join(globalConfigDir(), "templates"),
								filepath.Join(path, ".zk/templates"),
							},
							Styler: styler,
						})

						loader.RegisterHelper("style", hbhelpers.NewStyleHelper(styler, logger))
						loader.RegisterHelper("slug", hbhelpers.NewSlugHelper(language, logger))
						loader.RegisterHelper("sh", hbhelpers.NewShellHelper(logger, executil.ResolveShell(config.Tool.Shell)))

						linkFormatter, err := core.NewLinkFormatter(config.Format.Markdown, loader)
						if err != nil {
							return nil, err
						}
						loader.RegisterHelper("format-link", hbhelpers.NewLinkHelper(linkFormatter, logger))

						return loader, nil
					},
					IDGeneratorFactory: func(opts core.IDOptions) func() string {
						return rand.NewIDGenerator(opts)
					},
					FS:     fs,
					Logger: logger,
					OSEnv: func() map[string]string {
						return osutil.Env()
					},
				})

				return notebook, nil
			},
		}),
	}, nil
}

// locateGlobalConfig looks for the global zk config file following the
// XDG Base Directory specification
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
func locateGlobalConfig() (string, error) {
	if _, ok := os.LookupEnv("RUNNING_TESH"); ok {
		return "", nil
	}

	configPath := filepath.Join(globalConfigDir(), "config.toml")
	exists, err := paths.Exists(configPath)
	switch {
	case err != nil:
		return "", err
	case exists:
		return configPath, nil
	default:
		return "", nil
	}
}

// globalConfigDir returns the parent directory of the global configuration file.
func globalConfigDir() string {
	if path, ok := os.LookupEnv("ZK_CONFIG_DIR"); ok && path != "" {
		return path
	}
	path, ok := os.LookupEnv("XDG_CONFIG_HOME")
	if !ok {
		home, ok := os.LookupEnv("HOME")
		if !ok {
			home = "~/"
		}
		path = filepath.Join(home, ".config")
	}
	return filepath.Join(path, "zk")
}

// SetCurrentNotebook sets the first notebook found in the given search paths
// as the current default one.
func (c *Container) SetCurrentNotebook(searchDirs []Dirs) error {
	if len(searchDirs) == 0 {
		return nil
	}

	for _, dirs := range searchDirs {
		notebookDir := c.FS.Canonical(dirs.NotebookDir)
		workingDir := c.FS.Canonical(dirs.WorkingDir)

		c.currentNotebook, c.currentNotebookErr = c.Notebooks.Open(notebookDir)
		if c.currentNotebookErr == nil {
			c.setWorkingDir(workingDir)
			c.Config = c.currentNotebook.Config
			// FIXME: Is there something to do to support multiple notebooks here?
			os.Setenv("ZK_NOTEBOOK_DIR", c.currentNotebook.Path)
		}
		// Report the error only if it's not the "notebook not found" one.
		var errNotFound core.ErrNotebookNotFound
		if !errors.As(c.currentNotebookErr, &errNotFound) {
			return c.currentNotebookErr
		}
	}
	return nil
}

// SetWorkingDir resets the current working directory.
func (c *Container) setWorkingDir(path string) {
	path = c.FS.Canonical(path)
	c.WorkingDir = path
	c.FS.SetWorkingDir(path)
}

// CurrentNotebook returns the current default notebook.
func (c *Container) CurrentNotebook() (*core.Notebook, error) {
	return c.currentNotebook, c.currentNotebookErr
}

func (c *Container) NewNoteFilter(opts fzf.NoteFilterOpts) *fzf.NoteFilter {
	opts.PreviewCmd = c.Config.Tool.FzfPreview
	opts.LineTemplate = c.Config.Tool.FzfLine
	opts.FzfOptions = c.Config.Tool.FzfOptions
	opts.NewBinding = c.Config.Tool.FzfBindNew
	return fzf.NewNoteFilter(opts, c.FS, c.Terminal, c.TemplateLoader)
}

func (c *Container) NewNoteEditor(notebook *core.Notebook) (*editor.Editor, error) {
	shell := executil.ResolveShell(notebook.Config.Tool.Shell)
	return editor.NewEditor(notebook.Config.Tool.Editor, shell)
}

// Paginate creates an auto-closing io.Writer which will be automatically
// paginated if noPager is false, using the user's pager.
//
// You can write to the pager only in the run callback.
func (c *Container) Paginate(noPager bool, run func(out io.Writer) error) error {
	pager, err := c.pager(noPager || c.Config.Tool.Pager.IsEmpty())
	if err != nil {
		return err
	}
	err = run(pager)
	pager.Close()
	return err
}

func (c *Container) pager(noPager bool) (*pager.Pager, error) {
	if noPager || !c.Terminal.IsInteractive() {
		return pager.PassthroughPager, nil
	} else {
		shell := executil.ResolveShell(c.Config.Tool.Shell)
		return pager.New(c.Config.Tool.Pager, shell, c.Logger)
	}
}
