package state

import (
	"bytes"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	comatproto "github.com/bluesky-social/indigo/api/atproto"
	"github.com/bluesky-social/indigo/atproto/syntax"
	lexutil "github.com/bluesky-social/indigo/lex/util"
	indigoxrpc "github.com/bluesky-social/indigo/xrpc"
	"github.com/ipfs/go-cid"
	"github.com/multiformats/go-multihash"

	"tangled.org/core/api/tangled"
	"tangled.org/core/appview/db"
	"tangled.org/core/appview/models"
	"tangled.org/core/appview/pages"
	"tangled.org/core/orm"
	"tangled.org/core/tid"
)

func (s *State) CommentBodyFragment(w http.ResponseWriter, r *http.Request) {
	l := s.logger.With("handler", "CommentBodyFragment")
	user := s.oauth.GetMultiAccountUser(r)

	commentAt := r.URL.Query().Get("aturi")
	comment, err := db.GetComment(s.db, orm.FilterEq("at_uri", commentAt))
	if err != nil {
		l.Error("failed to fetch comment", "aturi", commentAt)
		http.Error(w, "Failed to fetch comment", http.StatusInternalServerError)
		return
	}

	reactions, err := db.GetReactionMap(s.db, 20, comment.FeedCommentAtUri())
	if err != nil {
		l.Error("failed to get reactions", "err", err)
	}
	var userReactions map[models.ReactionKind]bool
	if user != nil {
		userReactions, err = db.GetReactionStatusMap(s.db, syntax.DID(user.Did), comment.FeedCommentAtUri())
		if err != nil {
			l.Error("failed to get user reactions", "err", err)
		}
	}

	err = s.pages.CommentBodyFragment(w, pages.CommentBodyFragmentParams{
		Comment:     comment,
		Reactions:   reactions,
		UserReacted: userReactions,
	})
	if err != nil {
		l.Error("failed to render")
	}
}

func (s *State) EditCommentFragment(w http.ResponseWriter, r *http.Request) {
	l := s.logger.With("handler", "EditCommentFragment")

	commentAt := r.URL.Query().Get("aturi")
	comment, err := db.GetComment(s.db, orm.FilterEq("at_uri", commentAt))
	if err != nil {
		l.Error("failed to fetch comment", "aturi", commentAt)
		http.Error(w, "Failed to fetch comment", http.StatusInternalServerError)
		return
	}

	err = s.pages.EditCommentFragment(w, pages.EditCommentFragmentParams{
		Comment: comment,
	})
	if err != nil {
		l.Error("failed to render", "err", err)
	}
}

func (s *State) NewReplyCommentFragment(w http.ResponseWriter, r *http.Request) {
	s.pages.ReplyCommentFragment(w, pages.ReplyCommentFragmentParams{
		BaseParams: pages.BaseParamsFromContext(r.Context()),
	})
}

func (s *State) ReplyPlaceholderFragment(w http.ResponseWriter, r *http.Request) {
	s.pages.ReplyPlaceholderFragment(w, pages.ReplyPlaceholderFragmentParams{
		BaseParams: pages.BaseParamsFromContext(r.Context()),
	})
}

func (s *State) NewComment(w http.ResponseWriter, r *http.Request) {
	l := s.logger.With("handler", "NewComment")
	user := s.oauth.GetMultiAccountUser(r)

	noticeId := "comment-error"
	ctx := r.Context()

	var pullRoundIdx *int
	if pullRoundIdxRaw := r.FormValue("pull-round-idx"); pullRoundIdxRaw != "" {
		roundIdx, err := strconv.Atoi(pullRoundIdxRaw)
		if err != nil {
			l.Warn("invalid round idx", "err", err)
			s.pages.Notice(w, noticeId, "pull round index should be valid integer")
			return
		}
		pullRoundIdx = &roundIdx
		noticeId = fmt.Sprintf("comment-error-%d", roundIdx)
	}

	body := r.FormValue("body")
	if body == "" {
		s.pages.Notice(w, noticeId, "Body is required")
		return
	}

	// TODO(boltless): normalize markdown body
	normalizedBody := body
	mentions, references := s.mentionsResolver.Resolve(ctx, body)

	markdownBody := tangled.MarkupMarkdown{
		Text:     normalizedBody,
		Original: &body,
		Blobs:    models.ParseBlobs(r.PostForm["blobs"], normalizedBody),
	}

	subjectUri, err := syntax.ParseATURI(r.FormValue("subject-uri"))
	if err != nil {
		l.Warn("invalid subject uri", "err", err)
		s.pages.Notice(w, noticeId, "Subject URI should be valid AT-URI")
		return
	}
	l = l.With("subject.uri", subjectUri)

	// ingest CID of subject record on-demand.
	// TODO(boltless): appview should ingest CID of all atproto records
	var subjectCid syntax.CID
	if subjectCidRaw := r.FormValue("subject-cid"); subjectCidRaw != "" {
		subjectCid, err = syntax.ParseCID(subjectCidRaw)
		if err != nil {
			l.Warn("invalid subject cid", "err", err)
			s.pages.Notice(w, noticeId, "Subject CID should be valid CID")
			return
		}
	} else {
		l.Debug("fetching subject record CID")
		subjectCid, err = func(uri syntax.ATURI) (syntax.CID, error) {
			ident, err := s.idResolver.ResolveIdent(ctx, uri.Authority().String())
			if err != nil {
				return "", err
			}

			xrpcc := indigoxrpc.Client{Host: ident.PDSEndpoint()}
			out, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", uri.Collection().String(), ident.DID.String(), uri.RecordKey().String())
			if err != nil {
				return "", err
			}
			if out.Cid == nil {
				return "", fmt.Errorf("record CID is empty")
			}

			cid, err := syntax.ParseCID(*out.Cid)
			if err != nil {
				return "", err
			}

			return cid, nil
		}(subjectUri)
		if err != nil {
			l.Error("failed to backfill subject record", "err", err)
			s.pages.Notice(w, noticeId, "failed to backfill subject record")
			return
		}
	}
	l = l.With("subject.cid", subjectCid)

	subject := comatproto.RepoStrongRef{
		Uri: subjectUri.String(),
		Cid: subjectCid.String(),
	}

	var replyTo *comatproto.RepoStrongRef
	replyToUriRaw := r.FormValue("reply-to-uri")
	replyToCidRaw := r.FormValue("reply-to-cid")
	if replyToUriRaw != "" {
		replyToUri, err := syntax.ParseATURI(replyToUriRaw)
		if err != nil {
			s.pages.Notice(w, noticeId, "reply-to-uri should be valid AT-URI")
			return
		}
		// force replyTo.uri to `sh.tangled.feed.comment` collection, even when they aren't.
		// we are expecting parent comment will be migrated later.
		replyToUri = syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", replyToUri.Authority(), tangled.FeedCommentNSID, replyToUri.RecordKey()))

		var replyToCid syntax.CID
		if replyToCidRaw != "" {
			replyToCid, err = syntax.ParseCID(replyToCidRaw)
			if err != nil {
				s.pages.Notice(w, noticeId, "reply-to-cid should be valid CID")
				return
			}
		} else {
			// guess parent comment cid
			subjectComment, err := db.GetComment(s.db, orm.FilterEq("did", replyToUri.Authority()), orm.FilterEq("rkey", replyToUri.RecordKey()))
			if err != nil {
				l.Warn("db: failed to query subject comment", "err", err)
				s.pages.Notice(w, noticeId, "Subject record is unknown.")
				return
			}
			if subjectComment.Deleted != nil {
				// leave cid empty. reply comment won't pass the schema validation.
			} else {
				// guess cid from content
				c, err := func() (cid.Cid, error) {
					buf := new(bytes.Buffer)
					if subjectComment.Subject.Cid == "" {
						subjectComment.Subject.Cid = subject.Cid
					}
					if err := subjectComment.AsRecord().MarshalCBOR(buf); err != nil {
						return cid.Undef, fmt.Errorf("MarshalCBOR: %w", err)
					}
					return cid.NewPrefixV1(cid.DagCBOR, multihash.SHA2_256).Sum(buf.Bytes())
				}()
				if err != nil {
					l.Warn("cbor: failed to guess parent comment cid", "err", err)
					s.pages.Notice(w, noticeId, "Parent comment is invalid.")
					return
				}
				replyToCid = syntax.CID(c.String())
			}
		}
		replyTo = &comatproto.RepoStrongRef{
			Uri: replyToUri.String(),
			Cid: replyToCid.String(),
		}
	}

	comment := models.Comment{
		Did:        syntax.DID(user.Did),
		Collection: tangled.FeedCommentNSID,
		Rkey:       syntax.RecordKey(tid.TID()),

		Subject:      subject,
		Body:         markdownBody,
		Created:      time.Now(),
		ReplyTo:      replyTo,
		PullRoundIdx: pullRoundIdx,
	}
	if err = comment.Validate(); err != nil {
		l.Error("failed to validate comment", "err", err)
		s.pages.Notice(w, noticeId, "Failed to create comment.")
		return
	}

	client, err := s.oauth.AuthorizedClient(r)
	if err != nil {
		l.Error("failed to get authorized client", "err", err)
		s.pages.Notice(w, noticeId, "Failed to create comment.")
		return
	}

	// create a record first
	out, err := comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
		Collection: comment.Collection.String(),
		Repo:       comment.Did.String(),
		Rkey:       comment.Rkey.String(),
		Record:     &lexutil.LexiconTypeDecoder{Val: comment.AsRecord()},
	})
	if err != nil {
		l.Error("failed to create comment", "err", err)
		s.pages.Notice(w, noticeId, "Failed to create comment.")
		return
	}

	comment.Cid = syntax.CID(out.Cid)

	tx, err := s.db.Begin()
	if err != nil {
		l.Error("failed to start transaction", "err", err)
		s.pages.Notice(w, noticeId, "Failed to create comment, try again later.")
		return
	}
	defer tx.Rollback()

	_, err = db.PutComment(tx, &comment, references)
	if err != nil {
		l.Error("failed to create comment", "err", err)
		s.pages.Notice(w, noticeId, "Failed to create comment.")
		return
	}

	err = tx.Commit()
	if err != nil {
		l.Error("failed to commit transaction", "err", err)
		s.pages.Notice(w, noticeId, "Failed to create comment, try again later.")
		return
	}

	s.notifier.NewComment(ctx, &comment, mentions)

	if pullRoundIdx != nil {
		var buf bytes.Buffer
		if err := s.pages.PullCommentFragment(&buf, pages.PullCommentFragmentParams{
			LoggedInUser: user,
			Comment:      comment,
		}); err != nil {
			l.Error("failed to render pull comment fragment", "err", err)
		} else {
			w.Header().Set("Content-Type", "text/html")
			w.Write(buf.Bytes())
			return
		}
	}

	target, err := s.pages.MakeCommentUrl(ctx, comment.AtUri())
	if err != nil {
		s.pages.HxRefresh(w)
	}

	s.pages.HxLocation(w, target)
}

func (s *State) EditComment(w http.ResponseWriter, r *http.Request) {
	l := s.logger.With("handler", "EditComment")
	user := s.oauth.GetMultiAccountUser(r)

	noticeId := "comment-error"
	ctx := r.Context()

	commentAt := r.FormValue("aturi")
	comment, err := db.GetComment(s.db, orm.FilterEq("at_uri", commentAt))
	if err != nil {
		l.Error("failed to fetch comment", "aturi", commentAt, "err", err)
		s.pages.Notice(w, noticeId, "Failed to fetch comment")
		return
	}

	if comment.Did.String() != user.Did {
		l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did)
		s.pages.Notice(w, noticeId, "You are not the author of this comment")
		return
	}

	body := r.FormValue("body")
	if body == "" {
		s.pages.Notice(w, noticeId, "Body is required")
		return
	}

	// TODO(boltless): normalize markdown body
	normalizedBody := body
	_, references := s.mentionsResolver.Resolve(ctx, body)

	now := time.Now()
	newComment := comment
	newComment.Body = tangled.MarkupMarkdown{
		Text:     normalizedBody,
		Original: &body,
		Blobs:    nil,
	}
	newComment.Edited = &now
	if err := newComment.Validate(); err != nil {
		l.Error("failed to validate comment", "err", err)
		s.pages.Notice(w, noticeId, "Failed to update comment.")
		return
	}

	client, err := s.oauth.AuthorizedClient(r)
	if err != nil {
		l.Error("failed to get authorized client", "err", err)
		s.pages.Notice(w, noticeId, "Failed to create comment. try again later.")
		return
	}

	var existingBlobs []*lexutil.LexBlob
	if strings.Contains(normalizedBody, "blob+at://") {
		ex, err := comatproto.RepoGetRecord(ctx, client, "", newComment.Collection.String(), newComment.Did.String(), newComment.Rkey.String())
		if err != nil {
			l.Error("failed to read existing comment record for blob pinning", "err", err)
			s.pages.Notice(w, noticeId, "Failed to update comment, try again later.")
			return
		}
		if ex.Value != nil {
			if prev, ok := ex.Value.Val.(*tangled.FeedComment); ok && prev.Body != nil && prev.Body.MarkupMarkdown != nil {
				existingBlobs = prev.Body.MarkupMarkdown.Blobs
			}
		}
	}
	newComment.Body.Blobs = models.MergeBlobs(existingBlobs, r.PostForm["blobs"], normalizedBody)

	// update the record first
	exCid := comment.Cid.String()
	out, err := comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
		Collection: newComment.Collection.String(),
		Repo:       newComment.Did.String(),
		Rkey:       newComment.Rkey.String(),
		SwapRecord: &exCid,
		Record: &lexutil.LexiconTypeDecoder{
			Val: newComment.AsRecord(),
		},
	})
	if err != nil {
		l.Error("failed to update comment", "err", err)
		s.pages.Notice(w, noticeId, "Failed to update comment, try again later.")
		return
	}

	newComment.Cid = syntax.CID(out.Cid)

	tx, err := s.db.Begin()
	if err != nil {
		l.Error("failed to start transaction", "err", err)
		s.pages.Notice(w, noticeId, "Failed to update comment, try again later.")
		return
	}
	defer tx.Rollback()

	_, err = db.PutComment(tx, &newComment, references)
	if err != nil {
		l.Error("failed to perform update-description query", "err", err)
		s.pages.Notice(w, noticeId, "Failed to update comment, try again later.")
		return
	}
	err = tx.Commit()
	if err != nil {
		l.Error("failed to commit transaction", "err", err)
		s.pages.Notice(w, noticeId, "Failed to update comment, try again later.")
		return
	}

	reactions, err := db.GetReactionMap(s.db, 20, comment.FeedCommentAtUri())
	if err != nil {
		l.Error("failed to get reactions", "err", err)
	}
	userReactions, err := db.GetReactionStatusMap(s.db, syntax.DID(user.Did), comment.FeedCommentAtUri())
	if err != nil {
		l.Error("failed to get user reactions", "err", err)
	}

	// TODO: return full comment fragment so we can update comment header too
	s.pages.CommentBodyFragment(w, pages.CommentBodyFragmentParams{
		Comment:     newComment,
		Reactions:   reactions,
		UserReacted: userReactions,
	})
}

func (s *State) DeleteComment(w http.ResponseWriter, r *http.Request) {
	l := s.logger.With("handler", "DeleteComment")
	user := s.oauth.GetMultiAccountUser(r)

	noticeId := "comment"
	ctx := r.Context()

	commentAt := r.URL.Query().Get("aturi")
	comment, err := db.GetComment(s.db, orm.FilterEq("at_uri", commentAt))
	if err != nil {
		l.Error("failed to fetch comment", "aturi", commentAt)
		s.pages.Notice(w, noticeId, "Failed to fetch comment.")
		return
	}

	if comment.Did.String() != user.Did {
		l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did)
		s.pages.Notice(w, noticeId, "you are not the author of this comment")
		return
	}

	if comment.Deleted != nil {
		s.pages.Notice(w, noticeId, "Comment already deleted")
		return
	}

	client, err := s.oauth.AuthorizedClient(r)
	if err != nil {
		l.Error("failed to get authorized client", "err", err)
		s.pages.Notice(w, "comment", "Failed to delete comment.")
		return
	}
	_, err = comatproto.RepoDeleteRecord(ctx, client, &comatproto.RepoDeleteRecord_Input{
		Collection: comment.Collection.String(),
		Repo:       comment.Did.String(),
		Rkey:       comment.Rkey.String(),
	})
	if err != nil {
		l.Error("failed to delete from PDS", "err", err)
		s.pages.Notice(w, noticeId, "Failed to delete comment, try again later.")
		return
	}

	// optimistic update for htmx response
	now := time.Now()
	comment.Body = tangled.MarkupMarkdown{}
	comment.Deleted = &now

	s.pages.CommentBodyFragment(w, pages.CommentBodyFragmentParams{
		Comment: comment,
	})
	s.pages.CommentHeaderFragment(w, pages.CommentHeaderFragmentParams{
		Comment:   comment,
		HxSwapOob: true,
	})
}
