package models

import (
	"fmt"
	"log"
	"slices"
	"strings"
	"time"

	"github.com/bluesky-social/indigo/atproto/syntax"
	"tangled.org/core/api/tangled"
	"tangled.org/core/patchutil"
	"tangled.org/core/types"
)

type PullState int

const (
	PullClosed PullState = iota
	PullOpen
	PullMerged
	PullDeleted
)

func (p PullState) String() string {
	switch p {
	case PullOpen:
		return "open"
	case PullMerged:
		return "merged"
	case PullClosed:
		return "closed"
	case PullDeleted:
		return "deleted"
	default:
		return "closed"
	}
}

func (p PullState) IsOpen() bool {
	return p == PullOpen
}
func (p PullState) IsMerged() bool {
	return p == PullMerged
}
func (p PullState) IsClosed() bool {
	return p == PullClosed
}
func (p PullState) IsDeleted() bool {
	return p == PullDeleted
}

type Pull struct {
	// ids
	ID     int
	PullId int

	// at ids
	RepoAt   syntax.ATURI
	OwnerDid string
	Rkey     string

	// content
	Title        string
	Body         string
	TargetBranch string
	State        PullState
	Submissions  []*PullSubmission
	Mentions     []syntax.DID
	References   []syntax.ATURI

	// stacking
	StackId        string // nullable string
	ChangeId       string // nullable string
	ParentChangeId string // nullable string

	// meta
	Created    time.Time
	PullSource *PullSource

	// optionally, populate this when querying for reverse mappings
	Labels LabelState
	Repo   *Repo
}

// NOTE: This method does not include patch blob in returned atproto record
func (p Pull) AsRecord() tangled.RepoPull {
	var source *tangled.RepoPull_Source
	if p.PullSource != nil {
		source = &tangled.RepoPull_Source{}
		source.Branch = p.PullSource.Branch
		source.Sha = p.LatestSha()
		if p.PullSource.RepoAt != nil {
			s := p.PullSource.RepoAt.String()
			source.Repo = &s
		}
	}
	mentions := make([]string, len(p.Mentions))
	for i, did := range p.Mentions {
		mentions[i] = string(did)
	}
	references := make([]string, len(p.References))
	for i, uri := range p.References {
		references[i] = string(uri)
	}

	record := tangled.RepoPull{
		Title:      p.Title,
		Body:       &p.Body,
		Mentions:   mentions,
		References: references,
		CreatedAt:  p.Created.Format(time.RFC3339),
		Target: &tangled.RepoPull_Target{
			Repo:   p.RepoAt.String(),
			Branch: p.TargetBranch,
		},
		Source: source,
	}
	return record
}

type PullSource struct {
	Branch string
	RepoAt *syntax.ATURI

	// optionally populate this for reverse mappings
	Repo *Repo
}

type PullSubmission struct {
	// ids
	ID int

	// at ids
	PullAt syntax.ATURI

	// content
	RoundNumber int
	Patch       string
	Combined    string
	Comments    []PullComment
	SourceRev   string // include the rev that was used to create this submission: only for branch/fork PRs

	// meta
	Created time.Time
}

type PullComment struct {
	// ids
	ID           int
	PullId       int
	SubmissionId int

	// at ids
	RepoAt    string
	OwnerDid  string
	CommentAt string

	// content
	Body string

	// meta
	Mentions   []syntax.DID
	References []syntax.ATURI

	// meta
	Created time.Time
}

func (p *PullComment) AtUri() syntax.ATURI {
	return syntax.ATURI(p.CommentAt)
}

func (p *Pull) TotalComments() int {
	total := 0
	for _, s := range p.Submissions {
		total += len(s.Comments)
	}
	return total
}

func (p *Pull) LastRoundNumber() int {
	return len(p.Submissions) - 1
}

func (p *Pull) LatestSubmission() *PullSubmission {
	return p.Submissions[p.LastRoundNumber()]
}

func (p *Pull) LatestPatch() string {
	return p.LatestSubmission().Patch
}

func (p *Pull) LatestSha() string {
	return p.LatestSubmission().SourceRev
}

func (p *Pull) AtUri() syntax.ATURI {
	return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.OwnerDid, tangled.RepoPullNSID, p.Rkey))
}

func (p *Pull) IsPatchBased() bool {
	return p.PullSource == nil
}

func (p *Pull) IsBranchBased() bool {
	if p.PullSource != nil {
		if p.PullSource.RepoAt != nil {
			return p.PullSource.RepoAt == &p.RepoAt
		} else {
			// no repo specified
			return true
		}
	}
	return false
}

func (p *Pull) IsForkBased() bool {
	if p.PullSource != nil {
		if p.PullSource.RepoAt != nil {
			// make sure repos are different
			return p.PullSource.RepoAt != &p.RepoAt
		}
	}
	return false
}

func (p *Pull) IsStacked() bool {
	return p.StackId != ""
}

func (p *Pull) Participants() []string {
	participantSet := make(map[string]struct{})
	participants := []string{}

	addParticipant := func(did string) {
		if _, exists := participantSet[did]; !exists {
			participantSet[did] = struct{}{}
			participants = append(participants, did)
		}
	}

	addParticipant(p.OwnerDid)

	for _, s := range p.Submissions {
		for _, sp := range s.Participants() {
			addParticipant(sp)
		}
	}

	return participants
}

func (s PullSubmission) IsFormatPatch() bool {
	return patchutil.IsFormatPatch(s.Patch)
}

func (s PullSubmission) AsFormatPatch() []types.FormatPatch {
	patches, err := patchutil.ExtractPatches(s.Patch)
	if err != nil {
		log.Println("error extracting patches from submission:", err)
		return []types.FormatPatch{}
	}

	return patches
}

func (s *PullSubmission) Participants() []string {
	participantSet := make(map[string]struct{})
	participants := []string{}

	addParticipant := func(did string) {
		if _, exists := participantSet[did]; !exists {
			participantSet[did] = struct{}{}
			participants = append(participants, did)
		}
	}

	addParticipant(s.PullAt.Authority().String())

	for _, c := range s.Comments {
		addParticipant(c.OwnerDid)
	}

	return participants
}

func (s PullSubmission) CombinedPatch() string {
	if s.Combined == "" {
		return s.Patch
	}

	return s.Combined
}

type Stack []*Pull

// position of this pull in the stack
func (stack Stack) Position(pull *Pull) int {
	return slices.IndexFunc(stack, func(p *Pull) bool {
		return p.ChangeId == pull.ChangeId
	})
}

// all pulls below this pull (including self) in this stack
//
// nil if this pull does not belong to this stack
func (stack Stack) Below(pull *Pull) Stack {
	position := stack.Position(pull)

	if position < 0 {
		return nil
	}

	return stack[position:]
}

// all pulls below this pull (excluding self) in this stack
func (stack Stack) StrictlyBelow(pull *Pull) Stack {
	below := stack.Below(pull)

	if len(below) > 0 {
		return below[1:]
	}

	return nil
}

// all pulls above this pull (including self) in this stack
func (stack Stack) Above(pull *Pull) Stack {
	position := stack.Position(pull)

	if position < 0 {
		return nil
	}

	return stack[:position+1]
}

// all pulls below this pull (excluding self) in this stack
func (stack Stack) StrictlyAbove(pull *Pull) Stack {
	above := stack.Above(pull)

	if len(above) > 0 {
		return above[:len(above)-1]
	}

	return nil
}

// the combined format-patches of all the newest submissions in this stack
func (stack Stack) CombinedPatch() string {
	// go in reverse order because the bottom of the stack is the last element in the slice
	var combined strings.Builder
	for idx := range stack {
		pull := stack[len(stack)-1-idx]
		combined.WriteString(pull.LatestPatch())
		combined.WriteString("\n")
	}
	return combined.String()
}

// filter out PRs that are "active"
//
// PRs that are still open are active
func (stack Stack) Mergeable() Stack {
	var mergeable Stack

	for _, p := range stack {
		// stop at the first merged PR
		if p.State == PullMerged || p.State == PullClosed {
			break
		}

		// skip over deleted PRs
		if p.State != PullDeleted {
			mergeable = append(mergeable, p)
		}
	}

	return mergeable
}

type BranchDeleteStatus struct {
	Repo   *Repo
	Branch string
}
