package federation

import (
	"encoding/json"
	"fmt"
	"log"
	"strings"
	"time"

	"xorm.io/xorm"

	"github.com/forgeo/forgebucket/internal/models"
)

// RepoAPID returns the ActivityPub actor ID for a repository.
// Format: {instanceURL}/repos/{owner}/{name}
func RepoAPID(instanceURL, owner, name string) string {
	return strings.TrimRight(instanceURL, "/") + "/repos/" + owner + "/" + name
}

// RepoActorJSON builds the JSON-LD actor document for a ForgeFed Repository actor.
func RepoActorJSON(owner, name, description, instanceURL string) map[string]any {
	apid := RepoAPID(instanceURL, owner, name)
	return map[string]any{
		"@context": []any{
			"https://www.w3.org/ns/activitystreams",
			"https://w3id.org/security/v1",
			map[string]string{
				"Repository": "https://www.w3.org/ns/activitystreams#Repository",
			},
		},
		"id":                apid,
		"type":              "Repository",
		"preferredUsername": name,
		"name":              owner + "/" + name,
		"summary":           description,
		"inbox":             apid + "/inbox",
		"outbox":            apid + "/outbox",
		"followers":         apid + "/followers",
		"following":         apid + "/following",
	}
}

// HandleCreatePullRequest processes an incoming Create activity whose object
// is a PullRequest (per the ForgeFed vocabulary). It creates a local PR record
// in the target repository for the cross-instance proposal.
func HandleCreatePullRequest(db *xorm.Engine, body []byte, instanceURL string) error {
	var activity struct {
		Actor  string `json:"actor"`
		Object struct {
			Type    string `json:"type"`
			Summary string `json:"summary"`
			Content string `json:"content"`
			Source  *struct {
				ID   string `json:"id"`
				Name string `json:"name"`
			} `json:"source"`
			Target *struct {
				ID   string `json:"id"`
				Name string `json:"name"`
			} `json:"target"`
		} `json:"object"`
	}
	if err := json.Unmarshal(body, &activity); err != nil {
		return fmt.Errorf("parse activity: %w", err)
	}

	if activity.Object.Type != "PullRequest" {
		return nil
	}

	// Extract target repository info from the object's target.
	targetID := activity.Object.Target.ID
	targetParts := strings.Split(strings.TrimRight(targetID, "/"), "/")
	if len(targetParts) < 2 {
		return fmt.Errorf("cannot parse target repo APID: %s", targetID)
	}
	// Last two segments should be owner/repo-name.
	repoOwner := targetParts[len(targetParts)-2]
	repoName := targetParts[len(targetParts)-1]

	// Resolve the target repository.
	var repo models.Repository
	found, err := db.Where("name = ?", repoName).
		Join("INNER", "user", "repository.owner_id = user.id AND user.username = ?", repoOwner).
		Get(&repo)
	if err != nil {
		return fmt.Errorf("database error: %w", err)
	}
	if !found {
		return fmt.Errorf("target repo %s/%s not found on this instance", repoOwner, repoName)
	}

	// Resolve or create a FederationActor for the repo owner (needed for key ops).
	var ownerUser models.User
	if found, _ := db.Where("username = ?", repoOwner).Get(&ownerUser); !found {
		return fmt.Errorf("owner user %s not found", repoOwner)
	}
	localActor, err := GetOrCreate(db, ownerUser.ID, repoOwner, instanceURL)
	if err != nil {
		return fmt.Errorf("get actor: %w", err)
	}

	// Determine the PR title and body.
	title := activity.Object.Summary
	if title == "" {
		title = fmt.Sprintf("Cross-instance PR from %s", activity.Actor)
	}

	bodyContent := activity.Object.Content
	if bodyContent == "" {
		bodyContent = fmt.Sprintf("Pull request proposed via ActivityPub from %s", activity.Actor)
	}

	// Create the PR. For cross-instance PRs, authorID is set to the target
	// repo owner (we can't create a user for the remote actor automatically).
	// The RemoteSource field records the source repository APID.
	pr := &models.PullRequest{
		RepoID:       repo.ID,
		AuthorID:     ownerUser.ID,
		Title:        title,
		Body:         bodyContent,
		SourceBranch: "refs/for/main",
		TargetBranch: "main",
		Status:       models.PRStatusOpen,
		RemoteSource: activity.Actor,
	}

	// Try to extract source branch from the source repo.
	if activity.Object.Source != nil {
		sourceID := activity.Object.Source.ID
		if sourceID != "" {
			pr.RemoteSource = sourceID
		}
		if activity.Object.Source.Name != "" {
			pr.SourceBranch = activity.Object.Source.Name
		}
	}

	if _, err := db.Insert(pr); err != nil {
		return fmt.Errorf("insert PR: %w", err)
	}

	// Persist the outbound Accept for the PR activity so the remote knows
	// we received it (we auto-accept all incoming PRs).
	accept := map[string]any{
		"@context": "https://www.w3.org/ns/activitystreams",
		"id":       localActor.APID + "/activities/accept-pr-" + fmt.Sprint(time.Now().UnixNano()),
		"type":     "Accept",
		"actor":    localActor.APID,
	}
	acceptJSON, _ := json.Marshal(accept)
	db.Insert(&models.FederationActivity{ //nolint:errcheck
		ActorAPID:   localActor.APID,
		Type:        "Accept",
		ObjectJSON:  string(acceptJSON),
		Direction:   "outbound",
		RemoteActor: activity.Actor,
		Published:   time.Now().UTC(),
	})

	log.Printf("forgefed: created PR %d from cross-instance actor %s", pr.ID, activity.Actor)
	return nil
}

// SendCreatePullRequest delivers a Create(PullRequest) activity to a remote
// instance's inbox.  The remote inbox URL is derived from the forked-from repo's
// APID by appending /inbox.
func SendCreatePullRequest(db *xorm.Engine, localActor *models.FederationActor, pr *models.PullRequest, remoteAPID, instanceURL string) error {
	// Build the Create(PullRequest) activity.
	activity := map[string]any{
		"@context": "https://www.w3.org/ns/activitystreams",
		"id":       localActor.APID + "/activities/create-pr-" + fmt.Sprint(time.Now().UnixNano()),
		"type":     "Create",
		"actor":    localActor.APID,
		"object": map[string]any{
			"type":    "PullRequest",
			"id":      localActor.APID + "/pull-requests/" + fmt.Sprint(pr.ID),
			"summary": pr.Title,
			"content": pr.Body,
			"source": map[string]any{
				"type": "Repository",
				"id":   localActor.APID,
			},
			"target": map[string]any{
				"type": "Repository",
				"id":   remoteAPID,
			},
		},
		"to": []string{remoteAPID + "/inbox", "https://www.w3.org/ns/activitystreams#Public"},
	}

	remoteInbox := strings.TrimSuffix(remoteAPID, "/") + "/inbox"
	if err := DeliverActivity(localActor, activity, remoteInbox); err != nil {
		return fmt.Errorf("deliver PR to %s: %w", remoteInbox, err)
	}

	actJSON, _ := json.Marshal(activity)
	db.Insert(&models.FederationActivity{ //nolint:errcheck
		ActorAPID:   localActor.APID,
		Type:        "Create",
		ObjectJSON:  string(actJSON),
		Direction:   "outbound",
		RemoteActor: remoteAPID,
		Published:   time.Now().UTC(),
	})

	log.Printf("forgefed: sent Create(PullRequest) for PR %d to %s", pr.ID, remoteInbox)
	return nil
}

// IsCreatePullRequest checks whether the given body is a Create(PullRequest) activity.
func IsCreatePullRequest(body []byte) bool {
	var check struct {
		Type   string `json:"type"`
		Object struct {
			Type string `json:"type"`
		} `json:"object"`
	}
	if err := json.Unmarshal(body, &check); err != nil {
		return false
	}
	return check.Type == "Create" && check.Object.Type == "PullRequest"
}
