package handlers

import (
	"encoding/json"
	"io"
	"log"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"
	"xorm.io/xorm"

	"github.com/forgeo/forgebucket/internal/config"
	"github.com/forgeo/forgebucket/internal/domain/federation"
	"github.com/forgeo/forgebucket/internal/models"
)

const activityJSONType = "application/activity+json"

type FederationHandler struct {
	db  *xorm.Engine
	cfg *config.Config
}

func NewFederationHandler(db *xorm.Engine, cfg *config.Config) *FederationHandler {
	return &FederationHandler{db: db, cfg: cfg}
}

// WebFinger handles GET /.well-known/webfinger?resource=acct:user@domain
func (h *FederationHandler) WebFinger(w http.ResponseWriter, r *http.Request) {
	resource := r.URL.Query().Get("resource")
	if !strings.HasPrefix(resource, "acct:") {
		http.Error(w, "resource must use acct: scheme", http.StatusBadRequest)
		return
	}

	// acct:username@domain — extract username
	acct := strings.TrimPrefix(resource, "acct:")
	username := strings.SplitN(acct, "@", 2)[0]

	var user models.User
	if found, _ := h.db.Where("username = ?", username).Get(&user); !found {
		http.NotFound(w, r)
		return
	}

	actorURL := federation.APID(h.cfg.InstanceURL, username)
	resp := map[string]any{
		"subject": resource,
		"links": []map[string]any{
			{
				"rel":  "self",
				"type": activityJSONType,
				"href": actorURL,
			},
		},
	}

	w.Header().Set("Content-Type", "application/jrd+json")
	json.NewEncoder(w).Encode(resp) //nolint:errcheck
}

// Actor handles GET /users/{username} — returns the JSON-LD actor document.
func (h *FederationHandler) Actor(w http.ResponseWriter, r *http.Request) {
	username := chi.URLParam(r, "username")

	var user models.User
	if found, _ := h.db.Where("username = ?", username).Get(&user); !found {
		http.NotFound(w, r)
		return
	}

	actor, err := federation.GetOrCreate(h.db, user.ID, username, h.cfg.InstanceURL)
	if err != nil {
		http.Error(w, "could not get actor", http.StatusInternalServerError)
		return
	}

	doc := federation.ActorJSON(actor, username, username)
	w.Header().Set("Content-Type", activityJSONType)
	json.NewEncoder(w).Encode(doc) //nolint:errcheck
}

// Inbox handles POST /users/{username}/inbox — receive an ActivityPub activity.
func (h *FederationHandler) Inbox(w http.ResponseWriter, r *http.Request) {
	username := chi.URLParam(r, "username")

	var user models.User
	if found, _ := h.db.Where("username = ?", username).Get(&user); !found {
		http.NotFound(w, r)
		return
	}

	actor, err := federation.GetOrCreate(h.db, user.ID, username, h.cfg.InstanceURL)
	if err != nil {
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // 1 MB max
	if err != nil {
		http.Error(w, "could not read body", http.StatusBadRequest)
		return
	}

	// Verify HTTP signature. In debug mode, skip verification so local testing works.
	if !h.cfg.Debug {
		if err := federation.Verify(r, h.db, h.cfg.InstanceURL); err != nil {
			http.Error(w, "signature verification failed: "+err.Error(), http.StatusUnauthorized)
			return
		}
	}

	if err := federation.Receive(h.db, actor, body); err != nil {
		http.Error(w, "could not process activity: "+err.Error(), http.StatusBadRequest)
		return
	}

	w.WriteHeader(http.StatusAccepted)
}

// OutboxGet handles GET /users/{username}/outbox — serve the activity collection.
func (h *FederationHandler) OutboxGet(w http.ResponseWriter, r *http.Request) {
	username := chi.URLParam(r, "username")

	var user models.User
	if found, _ := h.db.Where("username = ?", username).Get(&user); !found {
		http.NotFound(w, r)
		return
	}

	actor, err := federation.GetOrCreate(h.db, user.ID, username, h.cfg.InstanceURL)
	if err != nil {
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}

	page, _ := strconv.Atoi(r.URL.Query().Get("page"))
	coll := federation.Collection(h.db, actor.APID, actor.OutboxURL, page)

	w.Header().Set("Content-Type", activityJSONType)
	json.NewEncoder(w).Encode(coll) //nolint:errcheck
}

// Followers handles GET /users/{username}/followers
func (h *FederationHandler) Followers(w http.ResponseWriter, r *http.Request) {
	username := chi.URLParam(r, "username")
	var user models.User
	if found, _ := h.db.Where("username = ?", username).Get(&user); !found {
		http.NotFound(w, r)
		return
	}
	actor, err := federation.GetOrCreate(h.db, user.ID, username, h.cfg.InstanceURL)
	if err != nil {
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	coll := federation.StubCollection(actor.APID + "/followers")
	w.Header().Set("Content-Type", activityJSONType)
	json.NewEncoder(w).Encode(coll) //nolint:errcheck
}

// RepoActor handles GET /repos/{owner}/{repo}/actor — returns the ForgeFed
// Repository actor document for cross-instance pull requests.
func (h *FederationHandler) RepoActor(w http.ResponseWriter, r *http.Request) {
	owner := chi.URLParam(r, "owner")
	repoName := chi.URLParam(r, "repo")

	var repo models.Repository
	if found, _ := h.db.Where("name = ?", repoName).
		Join("INNER", "user", "repository.owner_id = user.id AND user.username = ?", owner).
		Get(&repo); !found {
		http.NotFound(w, r)
		return
	}

	doc := federation.RepoActorJSON(owner, repoName, repo.Description, h.cfg.InstanceURL)
	w.Header().Set("Content-Type", activityJSONType)
	json.NewEncoder(w).Encode(doc) //nolint:errcheck
}

// RepoInbox handles POST /repos/{owner}/{repo}/inbox — receive ForgeFed
// activities for a repository (e.g. Create(PullRequest)).
func (h *FederationHandler) RepoInbox(w http.ResponseWriter, r *http.Request) {
	owner := chi.URLParam(r, "owner")
	repoName := chi.URLParam(r, "repo")

	var repo models.Repository
	if found, _ := h.db.Where("name = ?", repoName).
		Join("INNER", "user", "repository.owner_id = user.id AND user.username = ?", owner).
		Get(&repo); !found {
		http.NotFound(w, r)
		return
	}
	_ = repo

	body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
	if err != nil {
		http.Error(w, "could not read body", http.StatusBadRequest)
		return
	}

	// Determine the local repo actor APID.
	localActorAPID := federation.RepoAPID(h.cfg.InstanceURL, owner, repoName)

	// For repository inbox, we need a local actor for the repo owner.
	var ownerUser models.User
	if found, _ := h.db.Where("username = ?", owner).Get(&ownerUser); !found {
		http.Error(w, "owner not found", http.StatusInternalServerError)
		return
	}

	if !h.cfg.Debug {
		if err := federation.Verify(r, h.db, h.cfg.InstanceURL); err != nil {
			http.Error(w, "signature verification failed: "+err.Error(), http.StatusUnauthorized)
			return
		}
	}

	// Persist the activity.
	entry := &models.FederationActivity{
		ActorAPID:   localActorAPID,
		Type:        "Create",
		ObjectJSON:  string(body),
		Direction:   "inbound",
		RemoteActor: localActorAPID,
		Published:   time.Now().UTC(),
	}
	h.db.Insert(entry) //nolint:errcheck

	// Handle Create(PullRequest).
	if err := federation.HandleCreatePullRequest(h.db, body, h.cfg.InstanceURL); err != nil {
		log.Printf("federation: repo inbox handle: %v", err)
	}

	w.WriteHeader(http.StatusAccepted)
}
func (h *FederationHandler) Following(w http.ResponseWriter, r *http.Request) {
	username := chi.URLParam(r, "username")
	var user models.User
	if found, _ := h.db.Where("username = ?", username).Get(&user); !found {
		http.NotFound(w, r)
		return
	}
	actor, err := federation.GetOrCreate(h.db, user.ID, username, h.cfg.InstanceURL)
	if err != nil {
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	coll := federation.StubCollection(actor.APID + "/following")
	w.Header().Set("Content-Type", activityJSONType)
	json.NewEncoder(w).Encode(coll) //nolint:errcheck
}
