package strings

import (
	"fmt"
	"log/slog"
	"net/http"
	"path"
	"strconv"
	"time"

	"tangled.org/core/api/tangled"
	"tangled.org/core/appview/db"
	"tangled.org/core/appview/middleware"
	"tangled.org/core/appview/models"
	"tangled.org/core/appview/notify"
	"tangled.org/core/appview/oauth"
	"tangled.org/core/appview/pages"
	"tangled.org/core/appview/pages/markup"
	"tangled.org/core/idresolver"
	"tangled.org/core/orm"
	"tangled.org/core/tid"

	"github.com/bluesky-social/indigo/api/atproto"
	"github.com/bluesky-social/indigo/atproto/identity"
	"github.com/bluesky-social/indigo/atproto/syntax"
	"github.com/go-chi/chi/v5"

	comatproto "github.com/bluesky-social/indigo/api/atproto"
	lexutil "github.com/bluesky-social/indigo/lex/util"
)

type Strings struct {
	Db         *db.DB
	OAuth      *oauth.OAuth
	Pages      *pages.Pages
	IdResolver *idresolver.Resolver
	Logger     *slog.Logger
	Notifier   notify.Notifier
}

func (s *Strings) Router(mw *middleware.Middleware) http.Handler {
	r := chi.NewRouter()

	r.
		Get("/", s.timeline)

	r.
		With(mw.ResolveIdent()).
		Route("/{user}", func(r chi.Router) {
			r.Get("/", s.dashboard)

			r.Route("/{rkey}", func(r chi.Router) {
				r.Get("/", s.contents)
				r.Delete("/", s.delete)
				r.Get("/raw", s.contents)
				r.Get("/edit", s.edit)
				r.Post("/edit", s.edit)
			})
		})

	r.
		With(middleware.AuthMiddleware(s.OAuth)).
		Route("/new", func(r chi.Router) {
			r.Get("/", s.create)
			r.Post("/", s.create)
		})

	return r
}

func (s *Strings) timeline(w http.ResponseWriter, r *http.Request) {
	l := s.Logger.With("handler", "timeline")

	strings, err := db.GetStrings(s.Db, 50)
	if err != nil {
		l.Error("failed to fetch string", "err", err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	s.Pages.StringsTimeline(w, pages.StringTimelineParams{
		BaseParams: pages.BaseParamsFromContext(r.Context()),
		Strings:    strings,
	})
}

func (s *Strings) contents(w http.ResponseWriter, r *http.Request) {
	l := s.Logger.With("handler", "contents")

	id, ok := r.Context().Value("resolvedId").(identity.Identity)
	if !ok {
		l.Error("malformed middleware")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	l = l.With("did", id.DID, "handle", id.Handle)

	rkey := chi.URLParam(r, "rkey")
	if rkey == "" {
		l.Error("malformed url, empty rkey")
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	l = l.With("rkey", rkey)

	strings, err := db.GetStrings(
		s.Db,
		0,
		orm.FilterEq("did", id.DID),
		orm.FilterEq("rkey", rkey),
	)
	if err != nil {
		l.Error("failed to fetch string", "err", err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	if len(strings) < 1 {
		l.Error("string not found")
		s.Pages.Error404(w)
		return
	}
	if len(strings) != 1 {
		l.Error("incorrect number of records returned", "len(strings)", len(strings))
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	string := strings[0]

	if path.Base(r.URL.Path) == "raw" {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		if string.Filename != "" {
			w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", string.Filename))
		}
		w.Header().Set("Content-Length", strconv.Itoa(len(string.Contents)))

		_, err = w.Write([]byte(string.Contents))
		if err != nil {
			l.Error("failed to write raw response", "err", err)
		}
		return
	}

	var showRendered, renderToggle bool
	if markup.GetFormat(string.Filename) == markup.FormatMarkdown {
		renderToggle = true
		showRendered = r.URL.Query().Get("code") != "true"
	}

	stringUri := string.AtUri().String()
	starCount, err := db.GetStarCount(s.Db, models.StarSubjectString, stringUri)
	if err != nil {
		l.Error("failed to get star count", "err", err)
	}
	user := s.OAuth.GetMultiAccountUser(r)
	isStarred := false
	if user != nil {
		isStarred = db.GetStarStatus(s.Db, user.Did, stringUri)
	}

	comments, err := db.GetComments(s.Db, orm.FilterEq("subject_uri", string.AtUri()))
	if err != nil {
		l.Error("failed to get comments", "err", err)
	}

	var entities []syntax.ATURI
	for _, c := range comments {
		entities = append(entities, c.AtUri())
	}
	reactions, err := db.ListReactionDisplayDataMap(s.Db, entities, 20)
	if err != nil {
		l.Error("failed to get reactions", "err", err)
	}

	var userReactions map[syntax.ATURI]map[models.ReactionKind]bool
	if user != nil {
		userReactions, err = db.ListReactionStatusMap(s.Db, entities, syntax.DID(user.Did))
		if err != nil {
			l.Error("failed to get user reactions", "err", err)
		}
	}

	vouchRelationships := make(map[syntax.DID]*models.VouchRelationship)
	if user != nil {
		var participants []syntax.DID
		for _, c := range comments {
			participants = append(participants, c.Did)
		}
		vouchRelationships, err = db.GetVouchRelationshipsBatch(s.Db, syntax.DID(user.Did), participants)
		if err != nil {
			l.Error("failed to fetch vouch relationships", "err", err)
		}
	}

	err = s.Pages.SingleString(w, pages.SingleStringParams{
		BaseParams:   pages.BaseParamsFromContext(r.Context()),
		RenderToggle: renderToggle,
		ShowRendered: showRendered,
		String:       &string,
		Stats:        string.Stats(),
		IsStarred:    isStarred,
		StarCount:    starCount,
		Owner:        id,
		CommentList:  models.NewCommentList(comments),

		Reactions:          reactions,
		UserReacted:        userReactions,
		VouchRelationships: vouchRelationships,
	})
}

func (s *Strings) dashboard(w http.ResponseWriter, r *http.Request) {
	http.Redirect(w, r, fmt.Sprintf("/%s?tab=strings", chi.URLParam(r, "user")), http.StatusFound)
}

func (s *Strings) edit(w http.ResponseWriter, r *http.Request) {
	l := s.Logger.With("handler", "edit")

	user := s.OAuth.GetMultiAccountUser(r)

	id, ok := r.Context().Value("resolvedId").(identity.Identity)
	if !ok {
		l.Error("malformed middleware")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	l = l.With("did", id.DID, "handle", id.Handle)

	rkey := chi.URLParam(r, "rkey")
	if rkey == "" {
		l.Error("malformed url, empty rkey")
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	l = l.With("rkey", rkey)

	// get the string currently being edited
	all, err := db.GetStrings(
		s.Db,
		0,
		orm.FilterEq("did", id.DID),
		orm.FilterEq("rkey", rkey),
	)
	if err != nil {
		l.Error("failed to fetch string", "err", err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	if len(all) != 1 {
		l.Error("incorrect number of records returned", "len(strings)", len(all))
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	first := all[0]

	// verify that the logged in user owns this string
	if user.Did != id.DID.String() {
		l.Error("unauthorized request", "expected", id.DID, "got", user.Did)
		w.WriteHeader(http.StatusUnauthorized)
		return
	}

	switch r.Method {
	case http.MethodGet:
		// return the form with prefilled fields
		s.Pages.PutString(w, pages.PutStringParams{
			BaseParams: pages.BaseParamsFromContext(r.Context()),
			Action:     "edit",
			String:     first,
		})
	case http.MethodPost:
		fail := func(msg string, err error) {
			l.Error(msg, "err", err)
			s.Pages.Notice(w, "error", msg)
		}

		filename := r.FormValue("filename")
		if filename == "" {
			fail("Empty filename.", nil)
			return
		}

		content := r.FormValue("content")
		if content == "" {
			fail("Empty contents.", nil)
			return
		}

		description := r.FormValue("description")

		// construct new string from form values
		entry := models.String{
			Did:         first.Did,
			Rkey:        first.Rkey,
			Filename:    filename,
			Description: description,
			Contents:    content,
			Created:     first.Created,
		}

		record := entry.AsRecord()

		client, err := s.OAuth.AuthorizedClient(r)
		if err != nil {
			fail("Failed to create record.", err)
			return
		}

		// first replace the existing record in the PDS
		ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.StringNSID, entry.Did.String(), entry.Rkey)
		if err != nil {
			fail("Failed to updated existing record.", err)
			return
		}
		resp, err := comatproto.RepoPutRecord(r.Context(), client, &atproto.RepoPutRecord_Input{
			Collection: tangled.StringNSID,
			Repo:       entry.Did.String(),
			Rkey:       entry.Rkey,
			SwapRecord: ex.Cid,
			Record: &lexutil.LexiconTypeDecoder{
				Val: &record,
			},
		})
		if err != nil {
			fail("Failed to updated existing record.", err)
			return
		}
		l := l.With("aturi", resp.Uri)
		l.Info("edited string")

		// if that went okay, updated the db
		if err = db.AddString(s.Db, entry); err != nil {
			fail("Failed to update string.", err)
			return
		}

		s.Notifier.EditString(r.Context(), &entry)

		// if that went okay, redir to the string
		s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+entry.Rkey)
	}

}

func (s *Strings) create(w http.ResponseWriter, r *http.Request) {
	l := s.Logger.With("handler", "create")
	user := s.OAuth.GetMultiAccountUser(r)

	switch r.Method {
	case http.MethodGet:
		s.Pages.PutString(w, pages.PutStringParams{
			BaseParams: pages.BaseParamsFromContext(r.Context()),
			Action:     "new",
		})
	case http.MethodPost:
		fail := func(msg string, err error) {
			l.Error(msg, "err", err)
			s.Pages.Notice(w, "error", msg)
		}

		filename := r.FormValue("filename")
		if filename == "" {
			fail("Empty filename.", nil)
			return
		}

		content := r.FormValue("content")
		if content == "" {
			fail("Empty contents.", nil)
			return
		}

		description := r.FormValue("description")

		string := models.String{
			Did:         syntax.DID(user.Did),
			Rkey:        tid.TID(),
			Filename:    filename,
			Description: description,
			Contents:    content,
			Created:     time.Now(),
		}

		record := string.AsRecord()

		client, err := s.OAuth.AuthorizedClient(r)
		if err != nil {
			fail("Failed to create record.", err)
			return
		}

		resp, err := comatproto.RepoPutRecord(r.Context(), client, &atproto.RepoPutRecord_Input{
			Collection: tangled.StringNSID,
			Repo:       user.Did,
			Rkey:       string.Rkey,
			Record: &lexutil.LexiconTypeDecoder{
				Val: &record,
			},
		})
		if err != nil {
			fail("Failed to create record.", err)
			return
		}
		l := l.With("aturi", resp.Uri)
		l.Info("created record")

		// insert into DB
		if err = db.AddString(s.Db, string); err != nil {
			fail("Failed to create string.", err)
			return
		}

		s.Notifier.NewString(r.Context(), &string)

		// successful
		s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+string.Rkey)
	}
}

func (s *Strings) delete(w http.ResponseWriter, r *http.Request) {
	l := s.Logger.With("handler", "create")
	user := s.OAuth.GetMultiAccountUser(r)
	fail := func(msg string, err error) {
		l.Error(msg, "err", err)
		s.Pages.Notice(w, "error", msg)
	}

	id, ok := r.Context().Value("resolvedId").(identity.Identity)
	if !ok {
		l.Error("malformed middleware")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	l = l.With("did", id.DID, "handle", id.Handle)

	rkey := chi.URLParam(r, "rkey")
	if rkey == "" {
		l.Error("malformed url, empty rkey")
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	if user.Did != id.DID.String() {
		fail("You cannot delete this string", fmt.Errorf("unauthorized deletion, %s != %s", user.Did, id.DID.String()))
		return
	}

	client, err := s.OAuth.AuthorizedClient(r)
	if err != nil {
		fail("Failed to authorize client.", err)
		return
	}

	_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
		Collection: tangled.StringNSID,
		Repo:       user.Did,
		Rkey:       rkey,
	})
	if err != nil {
		fail("Failed to delete string record from PDS.", err)
		return
	}

	if err := db.DeleteString(
		s.Db,
		orm.FilterEq("did", user.Did),
		orm.FilterEq("rkey", rkey),
	); err != nil {
		fail("Failed to delete string.", err)
		return
	}

	s.Notifier.DeleteString(r.Context(), user.Did, rkey)

	s.Pages.HxRedirect(w, "/strings/"+user.Did)
}
