package nostrfs

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
	"time"
	"unsafe"

	"fiatjaf.com/lib/debouncer"
	"fiatjaf.com/nostr"
	"fiatjaf.com/nostr/nip19"
	"fiatjaf.com/nostr/nip27"
	"fiatjaf.com/nostr/nip73"
	"fiatjaf.com/nostr/nip92"
	sdk "fiatjaf.com/nostr/sdk"
	"github.com/fatih/color"
	"github.com/hanwen/go-fuse/v2/fs"
	"github.com/hanwen/go-fuse/v2/fuse"
)

type EntityDir struct {
	fs.Inode
	root *NostrRoot

	publisher *debouncer.Debouncer
	event     *nostr.Event
	updating  struct {
		title       string
		content     string
		publishedAt uint64
	}
}

var (
	_ = (fs.NodeOnAdder)((*EntityDir)(nil))
	_ = (fs.NodeGetattrer)((*EntityDir)(nil))
	_ = (fs.NodeSetattrer)((*EntityDir)(nil))
	_ = (fs.NodeCreater)((*EntityDir)(nil))
	_ = (fs.NodeUnlinker)((*EntityDir)(nil))
)

func (e *EntityDir) Getattr(_ context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
	out.Ctime = uint64(e.event.CreatedAt)
	if e.updating.publishedAt != 0 {
		out.Mtime = e.updating.publishedAt
	} else {
		out.Mtime = e.PublishedAt()
	}
	return fs.OK
}

func (e *EntityDir) Create(
	_ context.Context,
	name string,
	flags uint32,
	mode uint32,
	out *fuse.EntryOut,
) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
	if name == "publish" && e.publisher.IsRunning() {
		// this causes the publish process to be triggered faster
		log := e.root.ctx.Value("log").(func(msg string, args ...any))
		log("publishing now!\n")
		e.publisher.Flush()
		return nil, nil, 0, syscall.ENOTDIR
	}

	return nil, nil, 0, syscall.ENOTSUP
}

func (e *EntityDir) Unlink(ctx context.Context, name string) syscall.Errno {
	switch name {
	case "content" + kindToExtension(e.event.Kind):
		e.updating.content = e.event.Content
		return syscall.ENOTDIR
	case "title":
		e.updating.title = e.Title()
		return syscall.ENOTDIR
	default:
		return syscall.EINTR
	}
}

func (e *EntityDir) Setattr(_ context.Context, _ fs.FileHandle, in *fuse.SetAttrIn, _ *fuse.AttrOut) syscall.Errno {
	e.updating.publishedAt = in.Mtime
	return fs.OK
}

func (e *EntityDir) OnAdd(_ context.Context) {
	log := e.root.ctx.Value("log").(func(msg string, args ...any))

	e.AddChild("@author", e.NewPersistentInode(
		e.root.ctx,
		&fs.MemSymlink{
			Data: []byte(e.root.wd + "/" + nip19.EncodeNpub(e.event.PubKey)),
		},
		fs.StableAttr{Mode: syscall.S_IFLNK},
	), true)

	e.AddChild("event.json", e.NewPersistentInode(
		e.root.ctx,
		&DeterministicFile{
			get: func() (ctime uint64, mtime uint64, data string) {
				eventj, _ := json.MarshalIndent(e.event, "", "  ")
				return uint64(e.event.CreatedAt),
					uint64(e.event.CreatedAt),
					unsafe.String(unsafe.SliceData(eventj), len(eventj))
			},
		},
		fs.StableAttr{},
	), true)

	e.AddChild("identifier", e.NewPersistentInode(
		e.root.ctx,
		&fs.MemRegularFile{
			Data: []byte(e.event.Tags.GetD()),
			Attr: fuse.Attr{
				Mode:  0444,
				Ctime: uint64(e.event.CreatedAt),
				Mtime: uint64(e.event.CreatedAt),
				Size:  uint64(len(e.event.Tags.GetD())),
			},
		},
		fs.StableAttr{},
	), true)

	if e.root.signer == nil || e.root.rootPubKey != e.event.PubKey {
		// read-only
		e.AddChild("title", e.NewPersistentInode(
			e.root.ctx,
			&DeterministicFile{
				get: func() (ctime uint64, mtime uint64, data string) {
					return uint64(e.event.CreatedAt), e.PublishedAt(), e.Title()
				},
			},
			fs.StableAttr{},
		), true)
		e.AddChild("content."+kindToExtension(e.event.Kind), e.NewPersistentInode(
			e.root.ctx,
			&DeterministicFile{
				get: func() (ctime uint64, mtime uint64, data string) {
					return uint64(e.event.CreatedAt), e.PublishedAt(), e.event.Content
				},
			},
			fs.StableAttr{},
		), true)
	} else {
		// writeable
		e.updating.title = e.Title()
		e.updating.publishedAt = e.PublishedAt()
		e.updating.content = e.event.Content

		e.AddChild("title", e.NewPersistentInode(
			e.root.ctx,
			e.root.NewWriteableFile(e.updating.title, uint64(e.event.CreatedAt), e.updating.publishedAt, func(s string) {
				log("title updated")
				e.updating.title = strings.TrimSpace(s)
				e.handleWrite()
			}),
			fs.StableAttr{},
		), true)

		e.AddChild("content."+kindToExtension(e.event.Kind), e.NewPersistentInode(
			e.root.ctx,
			e.root.NewWriteableFile(e.updating.content, uint64(e.event.CreatedAt), e.updating.publishedAt, func(s string) {
				log("content updated")
				e.updating.content = strings.TrimSpace(s)
				e.handleWrite()
			}),
			fs.StableAttr{},
		), true)
	}

	var refsdir *fs.Inode
	i := 0
	for ref := range nip27.Parse(e.event.Content) {
		if _, isExternal := ref.Pointer.(nip73.ExternalPointer); isExternal {
			continue
		}
		i++

		if refsdir == nil {
			refsdir = e.NewPersistentInode(e.root.ctx, &fs.Inode{}, fs.StableAttr{Mode: syscall.S_IFDIR})
			e.root.AddChild("references", refsdir, true)
		}
		refsdir.AddChild(fmt.Sprintf("ref_%02d", i), refsdir.NewPersistentInode(
			e.root.ctx,
			&fs.MemSymlink{
				Data: []byte(e.root.wd + "/" + nip19.EncodePointer(ref.Pointer)),
			},
			fs.StableAttr{Mode: syscall.S_IFLNK},
		), true)
	}

	var imagesdir *fs.Inode
	addImage := func(url string) {
		if imagesdir == nil {
			in := &fs.Inode{}
			imagesdir = e.NewPersistentInode(e.root.ctx, in, fs.StableAttr{Mode: syscall.S_IFDIR})
			e.AddChild("images", imagesdir, true)
		}
		imagesdir.AddChild(filepath.Base(url), imagesdir.NewPersistentInode(
			e.root.ctx,
			&AsyncFile{
				ctx: e.root.ctx,
				load: func() ([]byte, nostr.Timestamp) {
					ctx, cancel := context.WithTimeout(e.root.ctx, time.Second*20)
					defer cancel()
					r, err := http.NewRequestWithContext(ctx, "GET", url, nil)
					if err != nil {
						log("failed to load image %s: %s\n", url, err)
						return nil, 0
					}
					resp, err := http.DefaultClient.Do(r)
					if err != nil {
						log("failed to load image %s: %s\n", url, err)
						return nil, 0
					}
					defer resp.Body.Close()
					if resp.StatusCode >= 300 {
						log("failed to load image %s: %s\n", url, err)
						return nil, 0
					}
					w := &bytes.Buffer{}
					io.Copy(w, resp.Body)
					return w.Bytes(), 0
				},
			},
			fs.StableAttr{},
		), true)
	}

	images := nip92.ParseTags(e.event.Tags)
	for _, imeta := range images {
		if imeta.URL == "" {
			continue
		}
		addImage(imeta.URL)
	}

	if tag := e.event.Tags.Find("image"); tag != nil {
		addImage(tag[1])
	}
}

func (e *EntityDir) IsNew() bool {
	return e.event.CreatedAt == 0
}

func (e *EntityDir) PublishedAt() uint64 {
	if tag := e.event.Tags.Find("published_at"); tag != nil {
		publishedAt, _ := strconv.ParseUint(tag[1], 10, 64)
		return publishedAt
	}
	return uint64(e.event.CreatedAt)
}

func (e *EntityDir) Title() string {
	if tag := e.event.Tags.Find("title"); tag != nil {
		return tag[1]
	}
	return ""
}

func (e *EntityDir) handleWrite() {
	log := e.root.ctx.Value("log").(func(msg string, args ...any))
	logverbose := e.root.ctx.Value("logverbose").(func(msg string, args ...any))

	if e.root.opts.AutoPublishArticlesTimeout.Hours() < 24*365 {
		if e.publisher.IsRunning() {
			log(", timer reset")
		}
		log(", publishing the ")
		if e.IsNew() {
			log("new")
		} else {
			log("updated")
		}
		log(" event in %d seconds...\n", int(e.root.opts.AutoPublishArticlesTimeout.Seconds()))
	} else {
		log(".\n")
	}
	if !e.publisher.IsRunning() {
		log("- `touch publish` to publish immediately\n")
		log("- `rm title content." + kindToExtension(e.event.Kind) + "` to erase and cancel the edits\n")
	}

	e.publisher.Call(func() {
		if e.Title() == e.updating.title && e.event.Content == e.updating.content {
			log("not modified, publish canceled.\n")
			return
		}

		evt := nostr.Event{
			Kind:      e.event.Kind,
			Content:   e.updating.content,
			Tags:      make(nostr.Tags, len(e.event.Tags)),
			CreatedAt: nostr.Now(),
		}
		copy(evt.Tags, e.event.Tags) // copy tags because that's the rule
		if e.updating.title != "" {
			if titleTag := evt.Tags.Find("title"); titleTag != nil {
				titleTag[1] = e.updating.title
			} else {
				evt.Tags = append(evt.Tags, nostr.Tag{"title", e.updating.title})
			}
		}

		// "published_at" tag
		publishedAtStr := strconv.FormatUint(e.updating.publishedAt, 10)
		if publishedAtStr != "0" {
			if publishedAtTag := evt.Tags.Find("published_at"); publishedAtTag != nil {
				publishedAtTag[1] = publishedAtStr
			} else {
				evt.Tags = append(evt.Tags, nostr.Tag{"published_at", publishedAtStr})
			}
		}

		// add "p" tags from people mentioned and "q" tags from events mentioned
		for ref := range nip27.Parse(evt.Content) {
			if _, isExternal := ref.Pointer.(nip73.ExternalPointer); isExternal {
				continue
			}

			tag := ref.Pointer.AsTag()
			key := tag[0]
			val := tag[1]
			if key == "e" || key == "a" {
				key = "q"
			}
			if existing := evt.Tags.FindWithValue(key, val); existing == nil {
				evt.Tags = append(evt.Tags, tag)
			}
		}

		// sign and publish
		if err := e.root.signer.SignEvent(e.root.ctx, &evt); err != nil {
			log("failed to sign: '%s'.\n", err)
			return
		}
		logverbose("%s\n", evt)

		relays := e.root.sys.FetchWriteRelays(e.root.ctx, e.root.rootPubKey)
		if len(relays) == 0 {
			relays = e.root.sys.FetchOutboxRelays(e.root.ctx, e.root.rootPubKey, 6)
		}

		log("publishing to %d relays... ", len(relays))
		success := false
		first := true
		for res := range e.root.sys.Pool.PublishMany(e.root.ctx, relays, evt) {
			cleanUrl, _ := strings.CutPrefix(res.RelayURL, "wss://")
			if !first {
				log(", ")
			}
			first = false

			if res.Error != nil {
				log("%s: %s", color.RedString(cleanUrl), res.Error)
			} else {
				success = true
				log("%s: ok", color.GreenString(cleanUrl))
			}
		}
		log("\n")

		if success {
			e.event = &evt
			log("event updated locally.\n")
			e.updating.publishedAt = uint64(evt.CreatedAt) // set this so subsequent edits get the correct value
		} else {
			log("failed.\n")
		}
	})
}

func (r *NostrRoot) FetchAndCreateEntityDir(
	parent fs.InodeEmbedder,
	extension string,
	pointer nostr.EntityPointer,
) (*fs.Inode, error) {
	event, _, err := r.sys.FetchSpecificEvent(r.ctx, pointer, sdk.FetchSpecificEventParameters{
		WithRelays: false,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to fetch: %w", err)
	}

	return r.CreateEntityDir(parent, event), nil
}

func (r *NostrRoot) CreateEntityDir(
	parent fs.InodeEmbedder,
	event *nostr.Event,
) *fs.Inode {
	return parent.EmbeddedInode().NewPersistentInode(
		r.ctx,
		&EntityDir{root: r, event: event, publisher: debouncer.New(r.opts.AutoPublishArticlesTimeout)},
		fs.StableAttr{Mode: syscall.S_IFDIR},
	)
}