package lsp

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

	"github.com/tliron/glsp"
	protocol "github.com/tliron/glsp/protocol_3_16"
	"github.com/zk-org/zk/internal/core"
	dateutil "github.com/zk-org/zk/internal/util/date"
	"github.com/zk-org/zk/internal/util/opt"
)

const cmdNew = "zk.new"

type cmdNewOpts struct {
	Title                   string             `json:"title"`
	Content                 string             `json:"content"`
	Dir                     string             `json:"dir"`
	Group                   string             `json:"group"`
	Template                string             `json:"template"`
	Extra                   map[string]string  `json:"extra"`
	Date                    string             `json:"date"`
	Edit                    jsonBoolean        `json:"edit"`
	DryRun                  jsonBoolean        `json:"dryRun"`
	Append                  bool               `json:"append"`
	InsertLinkAtLocation    *protocol.Location `json:"insertLinkAtLocation"`
	InsertContentAtLocation *protocol.Location `json:"insertContentAtLocation"`
}

func executeCommandNew(notebook *core.Notebook, documents *documentStore, context *glsp.Context, args []any) (any, error) {
	var opts cmdNewOpts
	if len(args) > 1 {
		arg, ok := args[1].(map[string]any)
		if !ok {
			return nil, fmt.Errorf("%s expects a dictionary of options as second argument, got: %v", cmdNew, args[1])
		}
		err := unmarshalJSON(arg, &opts)
		if err != nil {
			return nil, fmt.Errorf("failed to parse %s args, got: %v: %w", cmdNew, arg, err)
		}
	}

	date, err := dateutil.TimeFromNatural(opts.Date)
	if err != nil {
		return nil, fmt.Errorf("%s, failed to parse the `date` option: %w", opts.Date, err)
	}

	note, err := notebook.NewNote(core.NewNoteOpts{
		Title:     opt.NewNotEmptyString(opts.Title),
		Content:   opts.Content,
		Directory: opt.NewNotEmptyString(opts.Dir),
		Group:     opt.NewNotEmptyString(opts.Group),
		Template:  opt.NewNotEmptyString(opts.Template),
		Extra:     opts.Extra,
		DryRun:    bool(opts.DryRun),
		Date:      date,
	})
	if err != nil {
		var noteExists core.ErrNoteExists
		if !errors.As(err, &noteExists) {
			return nil, err
		}
		note, err = notebook.FindNote(core.NoteFindOpts{
			IncludeHrefs: []string{noteExists.Name},
		})
		if err != nil {
			return nil, err
		}
	}
	if note == nil {
		return nil, errors.New("zk.new could not generate a new note")
	}

	if opts.InsertContentAtLocation != nil {
		go context.Call(protocol.ServerWorkspaceApplyEdit, protocol.ApplyWorkspaceEditParams{
			Edit: protocol.WorkspaceEdit{
				Changes: map[string][]protocol.TextEdit{
					opts.InsertContentAtLocation.URI: {{Range: opts.InsertContentAtLocation.Range, NewText: note.RawContent}},
				},
			},
		}, nil)
	}

	if !opts.DryRun && opts.InsertLinkAtLocation != nil {
		minNote := note.AsMinimalNote()

		var prefix string
		// Inserts link inline after selected text.
		if opts.Append {
			r := opts.InsertLinkAtLocation.Range
			opts.InsertLinkAtLocation.Range = protocol.Range{
				Start: r.End,
				End:   r.End,
			}
			// Seperate last selected character and link with a space.
			prefix = " "
		}

		info := &linkInfo{
			note:     &minNote,
			location: opts.InsertLinkAtLocation,
			title:    &opts.Title,
			prefix:   prefix,
		}
		err := linkNote(notebook, documents, context, info)

		if err != nil {
			return nil, err
		}
	}

	absPath := filepath.Join(notebook.Path, note.Path)
	if !opts.DryRun && opts.Edit {
		go context.Call(protocol.ServerWindowShowDocument, protocol.ShowDocumentParams{
			URI:       pathToURI(absPath),
			TakeFocus: boolPtr(true),
		}, nil)
	}

	return map[string]any{
		"path":    absPath,
		"content": note.RawContent,
	}, nil
}
