package client

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"slices"
	"strings"
	"sync"
	"time"

	"github.com/nexu-io/looper/internal/config"
	githubinfra "github.com/nexu-io/looper/internal/infra/github"
	"github.com/nexu-io/looper/internal/network/protocol"
	"github.com/nexu-io/looper/internal/storage"
	"github.com/nexu-io/looper/internal/version"
)

type Status struct {
	Configured       bool                      `json:"configured"`
	NetworkID        string                    `json:"networkId,omitempty"`
	NodeID           string                    `json:"nodeId,omitempty"`
	NodeName         string                    `json:"nodeName,omitempty"`
	GitHub           protocol.GitHubIdentity   `json:"github"`
	CurrentGitHub    protocol.GitHubIdentity   `json:"currentGithub"`
	CloudReachable   bool                      `json:"cloudReachable"`
	Warnings         []string                  `json:"warnings,omitempty"`
	Lease            protocol.CoordinatorLease `json:"lease"`
	RoutedProjects   int                       `json:"routedProjects"`
	LocalProjects    int                       `json:"localProjects"`
	IdentityDrift    bool                      `json:"identityDrift"`
	DriftReason      string                    `json:"driftReason,omitempty"`
	IdentityFallback bool                      `json:"identityFallback,omitempty"`
	LeaseAction      string                    `json:"leaseAction,omitempty"`
	LeaseError       string                    `json:"leaseError,omitempty"`
	LeaseHolder      *protocol.Membership      `json:"leaseHolder,omitempty"`
	Membership       *protocol.Membership      `json:"membership,omitempty"`
	LastHeartbeatAt  *time.Time                `json:"lastHeartbeatAt,omitempty"`
}

type Manager struct {
	statePath string
	config    config.Config
	repos     *storage.Repositories
	gh        *githubinfra.Gateway
	client    *http.Client
	now       func() time.Time
	mu        sync.RWMutex
	status    Status
	cancel    context.CancelFunc
	done      chan struct{}
	started   bool
}

func NewManager(statePath string, cfg config.Config, repos *storage.Repositories, gh *githubinfra.Gateway) *Manager {
	return &Manager{statePath: statePath, config: cfg, repos: repos, gh: gh, client: &http.Client{Timeout: 10 * time.Second}, now: time.Now, done: make(chan struct{})}
}

func (m *Manager) Start(parent context.Context) error {
	if m.started {
		return nil
	}
	m.started = true
	state, err := LoadState(m.statePath)
	if err != nil {
		if os.IsNotExist(err) {
			m.mu.Lock()
			m.status = Status{}
			m.mu.Unlock()
			close(m.done)
			return nil
		}
		m.started = false
		return err
	}
	m.mu.Lock()
	m.status = Status{Configured: true, NetworkID: state.NetworkID, NodeID: state.NodeID, NodeName: state.NodeName, GitHub: state.GitHub}
	m.mu.Unlock()
	routed, _ := countProjectModes(m.config)
	if routed == 0 {
		_, local := countProjectModes(m.config)
		m.mu.Lock()
		m.status.RoutedProjects = routed
		m.status.LocalProjects = local
		m.mu.Unlock()
		close(m.done)
		return nil
	}
	ctx, cancel := context.WithCancel(parent)
	m.cancel = cancel
	go func() {
		defer close(m.done)
		m.tick(ctx, state)
		ticker := time.NewTicker(10 * time.Second)
		defer ticker.Stop()
		for {
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
				m.tick(ctx, state)
			}
		}
	}()
	return nil
}

func (m *Manager) Stop() {
	if !m.started {
		return
	}
	if m.cancel != nil {
		m.cancel()
	}
	<-m.done
}

func (m *Manager) Status() Status {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.status
}

func (m *Manager) tick(ctx context.Context, state LocalState) {
	current, _ := m.currentGitHubIdentity(ctx)
	routed, local := countProjectModes(m.config)
	capabilities := protocol.NodeCapabilities{Roles: supportedRoles(m.config), CoordinatorEligible: routed > 0 && m.config.Roles.Coordinator.Enabled, RoutedProjects: routed, RoutedProjectIDs: routedProjectIDs(m.config), ReviewerProjects: reviewerProjectCapabilities(m.config), LocalProjects: local, DynamicLoad: m.dynamicLoad(ctx)}
	drift, reason := identityDrift(state.GitHub, current)
	capabilities.IdentityDrift = drift
	capabilities.DriftReason = reason
	m.mu.Lock()
	m.status.IdentityDrift = drift
	m.status.DriftReason = reason
	m.mu.Unlock()
	api := New(state.URL, state.NodeToken, m.client)
	hb, err := api.Heartbeat(ctx, protocol.HeartbeatRequest{ProtocolVersion: protocol.CurrentVersion, DaemonVersion: version.Value, NodeName: state.NodeName, GitHub: current, Capabilities: capabilities})
	if err != nil {
		m.mu.Lock()
		m.status.CloudReachable = false
		m.status.CurrentGitHub = current
		m.status.RoutedProjects = routed
		m.status.LocalProjects = local
		m.mu.Unlock()
		return
	}
	m.mu.Lock()
	m.status.CloudReachable = true
	m.status.CurrentGitHub = current
	m.status.Warnings = hb.Warnings
	m.status.RoutedProjects = routed
	m.status.LocalProjects = local
	m.status.LastHeartbeatAt = &hb.RecordedAt
	m.mu.Unlock()
	status, err := api.Status(ctx)
	if err == nil {
		lease := status.Lease
		leaseAction, leaseErr := m.reconcileLease(ctx, api, state, current, capabilities, status)
		if leaseAction != "" || leaseErr != "" {
			refreshed, refreshErr := api.Status(ctx)
			if refreshErr == nil {
				status = refreshed
				lease = refreshed.Lease
			}
		}
		warnings := append([]string{}, status.Warnings...)
		if current.Login != "" && current.NumericID == 0 {
			warnings = append(warnings, "current GitHub identity is using login fallback because no numeric ID is available")
		}
		m.mu.Lock()
		m.status.Lease = lease
		m.status.LeaseAction = leaseAction
		m.status.LeaseError = leaseErr
		m.status.LeaseHolder = findMembershipByNodeID(status.Memberships, lease.HolderNodeID)
		m.status.Membership = &status.Membership
		m.status.Warnings = warnings
		m.status.IdentityFallback = current.Login != "" && current.NumericID == 0
		m.mu.Unlock()
	}
}

func (m *Manager) reconcileLease(ctx context.Context, api *Client, state LocalState, current protocol.GitHubIdentity, capabilities protocol.NodeCapabilities, status protocol.NodeStatusResponse) (string, string) {
	eligible := capabilities.CoordinatorEligible && !capabilities.IdentityDrift && current.Login != "" && current.NumericID > 0
	holdsLease := status.Lease.HolderNodeID != "" && status.Lease.HolderNodeID == state.NodeID && status.Lease.ExpiresAt != nil && status.Lease.ExpiresAt.After(m.now().UTC())
	if holdsLease && !eligible {
		if _, err := api.ExpireLease(ctx, status.Lease.FencingToken); err != nil {
			return "expire_failed", err.Error()
		}
		return "expired", ""
	}
	if !eligible {
		return "", ""
	}
	if holdsLease {
		if _, err := api.RenewLease(ctx, protocol.CoordinatorLeaseRenewRequest{FencingToken: status.Lease.FencingToken}); err != nil {
			return "renew_failed", err.Error()
		}
		return "renewed", ""
	}
	if status.Lease.HolderNodeID == "" || status.Lease.ExpiresAt == nil || !status.Lease.ExpiresAt.After(m.now().UTC()) {
		if _, err := api.AcquireLease(ctx, protocol.CoordinatorLeaseAcquireRequest{}); err != nil {
			return "acquire_failed", err.Error()
		}
		return "acquired", ""
	}
	return "", ""
}

func findMembershipByNodeID(memberships []protocol.Membership, nodeID string) *protocol.Membership {
	nodeID = strings.TrimSpace(nodeID)
	if nodeID == "" {
		return nil
	}
	for _, member := range memberships {
		if strings.TrimSpace(member.NodeID) == nodeID {
			copy := member
			return &copy
		}
	}
	return nil
}

func (m *Manager) currentGitHubIdentity(ctx context.Context) (protocol.GitHubIdentity, error) {
	if m.gh == nil {
		return protocol.GitHubIdentity{}, nil
	}
	identity, err := m.gh.GetCurrentUserIdentity(ctx, "")
	if err != nil {
		return protocol.GitHubIdentity{}, err
	}
	return protocol.GitHubIdentity{NumericID: identity.NumericID, Login: identity.Login}, nil
}

func (m *Manager) dynamicLoad(ctx context.Context) int {
	if m.repos == nil || m.repos.Runs == nil {
		return 0
	}
	runs, err := m.repos.Runs.List(ctx)
	if err != nil {
		return 0
	}
	total := 0
	for _, run := range runs {
		if run.Status == "running" {
			total++
		}
	}
	return total
}

func countProjectModes(cfg config.Config) (routed int, local int) {
	for _, project := range cfg.Projects {
		if project.Network.Mode == config.ProjectNetworkModeRouted {
			routed++
		} else {
			local++
		}
	}
	return routed, local
}

func supportedRoles(cfg config.Config) []string {
	roles := []string{}
	if cfg.Roles.Coordinator.Enabled {
		roles = append(roles, "coordinator")
	}
	if cfg.Roles.Worker.AutoDiscovery {
		roles = append(roles, "worker")
	}
	if cfg.Roles.Reviewer.Discovery.AutoDiscovery {
		roles = append(roles, "reviewer")
	}
	return roles
}

func routedProjectIDs(cfg config.Config) []string {
	ids := make([]string, 0, len(cfg.Projects))
	for _, project := range cfg.Projects {
		if project.Network.Mode == config.ProjectNetworkModeRouted && strings.TrimSpace(project.ID) != "" {
			ids = append(ids, strings.TrimSpace(project.ID))
		}
	}
	slices.Sort(ids)
	return ids
}

func reviewerProjectCapabilities(cfg config.Config) []protocol.ReviewerProjectCapability {
	capabilities := make([]protocol.ReviewerProjectCapability, 0, len(cfg.Projects))
	for _, project := range cfg.Projects {
		if project.Network.Mode != config.ProjectNetworkModeRouted || strings.TrimSpace(project.ID) == "" {
			continue
		}
		roles := config.ProjectRoleConfigs(cfg, project.ID)
		if !roles.Reviewer.Discovery.AutoDiscovery {
			continue
		}
		requireReviewRequest := roles.Reviewer.Discovery.Triggers.RequireReviewRequest
		capabilities = append(capabilities, protocol.ReviewerProjectCapability{ProjectID: strings.TrimSpace(project.ID), IncludeDrafts: roles.Reviewer.Discovery.Triggers.IncludeDrafts, RequireReviewRequest: &requireReviewRequest, EnableSelfReview: roles.Reviewer.Discovery.Triggers.EnableSelfReview, Labels: append([]string(nil), roles.Reviewer.Discovery.Triggers.Labels...), LabelMode: string(roles.Reviewer.Discovery.Triggers.LabelMode)})
	}
	slices.SortFunc(capabilities, func(a, b protocol.ReviewerProjectCapability) int {
		return strings.Compare(a.ProjectID, b.ProjectID)
	})
	return capabilities
}

func identityDrift(expected, current protocol.GitHubIdentity) (bool, string) {
	if expected.NumericID > 0 && current.NumericID > 0 && expected.NumericID != current.NumericID {
		return true, fmt.Sprintf("stored GitHub numeric ID %d differs from current %d", expected.NumericID, current.NumericID)
	}
	if expected.Login != "" && current.Login != "" && expected.Login != current.Login {
		return true, fmt.Sprintf("stored GitHub login %q differs from current %q", expected.Login, current.Login)
	}
	return false, ""
}
