package recon

import (
	"errors"
	"fmt"
	"sort"

	github_recon_settings "github.com/anotherhadi/github-recon/settings"
	"github.com/anotherhadi/github-recon/utils"
	"github.com/google/go-github/v72/github"
)

type CloseFriendsResult []CloseFriendResult

type CloseFriendResult struct {
	Name     string
	Username string
	Orgs     []string
	Company  string
	Location string
	Score    int
}

const (
	maxTargetFollowing       = 50
	maxFollowersForCandidate = 20
	pointsPerCondition       = 1
)

// CloseFriends returns a list of "close friends" for the target user.
// A candidate is considered closer if:
// 1. The target follows fewer than 50 people.
// 2. The candidate has fewer than 20 followers (+1 point).
// 3. The candidate follows the target back (+1 point).
// 4. The candidate shares at least one organization with the target (+1 point).
func CloseFriends(s github_recon_settings.Settings) (results CloseFriendsResult) {
	targetFollowing, resp, err := s.Client.Users.ListFollowing(s.Ctx, s.Target, &github.ListOptions{PerPage: 100})
	if err != nil {
		s.Logger.Error("Failed to fetch target's following list", "err", err)
		return
	}
	utils.WaitForRateLimit(s, resp)

	targetOrgs, err := getOrgs(s, s.Target)
	if err != nil {
		s.Logger.Error("Failed to fetch target's organizations", "err", err)
		targetOrgs = []*github.Organization{}
	}

	if len(targetFollowing) > maxTargetFollowing {
		s.Logger.Info("Skipping close friends check",
			"reason",
			fmt.Sprintf("Target follows %d or more users (limit: %d)", len(targetFollowing), maxTargetFollowing),
		)
		return
	}

	if len(targetFollowing) == 0 {
		return
	}

	for _, candidate := range targetFollowing {
		candidateLogin := candidate.GetLogin()
		if candidateLogin == "" {
			continue
		}

		score := 0

		candidateDetails, userResp, err := s.Client.Users.Get(s.Ctx, candidateLogin)
		if err != nil {
			s.Logger.Warn("Failed to fetch details for candidate",
				"candidate", candidateLogin,
				"err", err,
			)
			if userResp != nil {
				utils.WaitForRateLimit(s, userResp)
			}
			continue
		}
		utils.WaitForRateLimit(s, userResp)

		// Condition: candidate has few followers
		if candidateDetails.GetFollowers() < maxFollowersForCandidate {
			score += pointsPerCondition
		}

		// Condition: candidate follows target back
		followsBack, err := checkIfUserFollows(s, candidateLogin, s.Target)
		if err == nil && followsBack {
			score += pointsPerCondition
		}

		// Condition: same organization
		candidateOrgs, _ := getOrgs(s, candidateLogin)
		if isInSameOrg(targetOrgs, candidateOrgs) {
			score += pointsPerCondition
		}

		// Add candidate if they matched at least one condition
		if score > 0 {
			results = append(results, CloseFriendResult{
				Name:     candidateDetails.GetName(),
				Username: candidateLogin,
				Orgs:     candidateOrgsToNames(candidateOrgs),
				Score:    score,
				Location: candidateDetails.GetLocation(),
				Company:  candidateDetails.GetCompany(),
			})
		}
	}

	if len(results) > 0 {
		sort.Slice(results, func(i, j int) bool {
			return results[i].Score > results[j].Score
		})
	}

	return
}

// checkIfUserFollows checks if sourceUser follows targetUser.
func checkIfUserFollows(s github_recon_settings.Settings, sourceUser, targetUser string) (bool, error) {
	isFollowing, resp, err := s.Client.Users.IsFollowing(s.Ctx, sourceUser, targetUser)
	if err != nil {
		s.Logger.Warn("Error checking if user follows target",
			"source", sourceUser,
			"target", targetUser,
			"err", err,
		)
		if resp != nil {
			utils.WaitForRateLimit(s, resp)
		}
		return false, err
	}
	if resp != nil {
		utils.WaitForRateLimit(s, resp)
	}
	return isFollowing, nil
}
func candidateOrgsToNames(orgs []*github.Organization) []string {
	var orgNames []string
	for _, org := range orgs {
		if org.GetLogin() != "" {
			orgNames = append(orgNames, org.GetLogin())
		}
	}
	return orgNames
}

func getOrgs(s github_recon_settings.Settings, user string) ([]*github.Organization, error) {
	orgs, resp, err := s.Client.Organizations.List(s.Ctx, user, nil)
	if err != nil {
		return nil, errors.New("failed to fetch organizations for user")
	}
	utils.WaitForRateLimit(s, resp)
	return orgs, nil
}

func isInSameOrg(orgsA, orgsB []*github.Organization) bool {
	for _, orgA := range orgsA {
		for _, orgB := range orgsB {
			if orgA.GetLogin() == orgB.GetLogin() {
				return true
			}
		}
	}
	return false
}
