// Package skill resolves per-agent skill bindings from the MeshX
// /api/v1/skills/* API. It mirrors the provisioning/memory pattern: each agent's
// owning AgentTeam server runtime is resolved to obtain the server URL and auth token,
// then POST /api/v1/skills/agents/access:batch returns the agent's accessible
// skills with the repository identity needed to compose a clone URL.
package skill

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"strings"
	"time"

	gittransport "github.com/go-git/go-git/v5/plumbing/transport/http"
	"github.com/tidbcloud/AgentTeam/desktop/internal/dispatcher"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/desktop/internal/provisioning/asset"
	"github.com/tidbcloud/AgentTeam/desktop/internal/provisioning/gitcred"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

// ServerConnectionResolver resolves an agent's owning AgentTeam server runtime connection info.
// The dispatcher provides this so the provider can route each agent to the
// correct AgentTeam server.
type ServerConnectionResolver interface {
	// ResolveServerForAgent returns the connection info for the AgentTeam server runtime
	// that owns the given agent, including a freshly-cached user session token.
	// Returns false when the agent has no AgentTeam server runtime or the channel is not running.
	ResolveServerForAgent(ctx context.Context, agentID string) (asset.ConnectionInfo, bool)
}

// SkillAccess describes one accessible skill for an agent: the skill identity,
// the AGS repository ref needed to compose a clone URL, and the branch to check
// out.
type SkillAccess struct {
	// SkillID is the MeshX skill identifier.
	SkillID string
	// Name is the human-readable display name.
	Name string
	// Branch is the single branch this skill repository is bound to locally.
	Branch string
	// CloneURL is the fully composed HTTPS clone URL
	// (serverURL + "/" + repoFullName + ".git").
	CloneURL string
	// RepoID is the AGS repository identifier (e.g. "sk_xxx"). Stored on the
	// Desktop-side ManagedSkillRepositoryRecord so the local cache can be keyed
	// by repo rather than by skill — the same AGS repo may back multiple
	// skills, and the local clone is per-repo.
	RepoID string
	// Auth supplies the AGS repository sync lease to go-git. The production
	// auth method sends Authorization: Bearer <sync lease>; nil is fine for
	// file:// test origins.
	Auth gittransport.AuthMethod
}

// LookupState is the three-state result of a binding lookup. It is the
// canonical alias for dispatcher.SkillLookupState so provisioning/skill consumers
// can keep using a stable local name while the dispatcher interface picks up
// the same underlying type.
//
// Treating Unknown as Unbound would wipe cached paths on transient
// resolver/token failures.
type LookupState = dispatcher.SkillLookupState

// LookupState values. Re-exported from dispatcher so existing callers do not
// need to update every reference when the canonical type moves.
const (
	LookupUnknown = dispatcher.SkillLookupUnknown
	LookupUnbound = dispatcher.SkillLookupUnbound
	LookupBound   = dispatcher.SkillLookupBound
)

// CacheState is the canonical alias for dispatcher.skillCacheState exposed
// via the SkillSyncer interface. Cached snapshots distinguish miss from
// authoritative empty so launch/execution paths can stay cache-only.
type CacheState = dispatcher.SkillCacheState

// CacheState values. Re-exported from dispatcher so sync callers can use the
// unqualified names that match the doc strings (CachePopulated etc).
const (
	CacheMiss               = dispatcher.SkillCacheMiss
	CacheAuthoritativeEmpty = dispatcher.SkillCacheAuthoritativeEmpty
	CachePopulated          = dispatcher.SkillCachePopulated
)

// LookupResult bundles the outcome of a single agent binding lookup. The
// State field is authoritative — see LookupState for semantics.
type LookupResult struct {
	Access []SkillAccess
	State  LookupState
	Err    error
}

// Provider resolves skill bindings for agents by calling the AgentTeam server's
// /api/v1/skills/agents/access:batch endpoint.
type Provider struct {
	resolver   ServerConnectionResolver
	httpClient *http.Client
}

// New creates a Provider that resolves each agent's server runtime via resolver.
func New(resolver ServerConnectionResolver) *Provider {
	return &Provider{
		resolver:   resolver,
		httpClient: &http.Client{Timeout: 30 * time.Second},
	}
}

// batchResponse mirrors the relevant fields of the server-side
// AgentSkillAccessBatchResponse DTO.
type batchResponse struct {
	Agents           map[string][]*accessibleSkill `json:"agents"`
	SyncToken        string                        `json:"syncToken,omitempty"`
	SyncTokenExpires string                        `json:"syncTokenExpires,omitempty"`
}

// accessibleSkill mirrors the relevant fields of AgentAccessibleSkill.
type accessibleSkill struct {
	ID         string         `json:"id"`
	Name       string         `json:"name"`
	Branch     string         `json:"branch"`
	Repository *repositoryRef `json:"repository,omitempty"`
}

// repositoryRef mirrors the relevant fields of ResourceRepositoryRef.
type repositoryRef struct {
	RepoID       string `json:"repoId,omitempty"`
	RepoOwner    string `json:"repoOwner"`
	RepoName     string `json:"repoName"`
	RepoFullName string `json:"repoFullName"`
}

// Lookup resolves the skill bindings for a single agent and reports the
// authoritative-ness of the result. Callers MUST consult the returned
// LookupState before changing cached/dispatched state.
//
// Return contract:
//   - (access, LookupBound, nil): the agent has accessible skills. The access
//     slice is the authoritative binding set; an empty slice with LookupBound
//     never happens (Bound implies at least one entry).
//   - (nil, LookupUnbound, nil): the server authoritatively reports the agent
//     has no accessible skills. Safe to clear caches and stop exposing managed
//     skill paths.
//   - (nil, LookupUnknown, nil): the resolver, channel, token, or batch
//     endpoint could not be reached. Callers MUST NOT change cached state —
//     treating Unknown as Unbound would wipe paths on transient token refresh
//     failures.
//   - (nil, LookupUnknown, err): same as the prior case but with a wrapped
//     error suitable for diagnostics. State changes remain forbidden.
func (p *Provider) Lookup(ctx context.Context, agentID string) ([]SkillAccess, LookupState, error) {
	agentID = strings.TrimSpace(agentID)
	if agentID == "" {
		// An empty agentID is a caller bug, not a binding change. Treat as
		// Unknown so callers do not wipe state for an unrelated scope.
		return nil, LookupUnknown, nil
	}

	lookup, err := p.lookupBatch(ctx, agentID)
	if err != nil {
		slog.WarnContext(ctx, "skillprovider: batch access request failed",
			"agent_id", agentID, "error", err)
		return nil, LookupUnknown, err
	}
	batchResp := lookup.response
	serverURL := lookup.serverURL
	workspaceID := lookup.workspaceID
	slog.InfoContext(ctx, "skillprovider: querying batch access",
		"agent_id", agentID, "endpoint", lookup.endpoint)

	rawSkills, ok := batchResp.Agents[agentID]
	if !ok {
		return nil, LookupUnknown, fmt.Errorf("skillprovider: batch response omitted requested agent %s", agentID)
	}
	access := make([]SkillAccess, 0, len(rawSkills))
	for _, s := range rawSkills {
		if s == nil {
			continue
		}
		cloneURL := composeCloneURL(serverURL, workspaceID, s.Repository)
		if cloneURL == "" {
			slog.WarnContext(ctx, "skillprovider: skipping skill with incomplete repository ref",
				"agent_id", agentID, "skill_id", s.ID)
			continue
		}
		branch := strings.TrimSpace(s.Branch)
		if branch == "" {
			slog.WarnContext(ctx, "skillprovider: skipping skill with empty branch",
				"agent_id", agentID, "skill_id", s.ID)
			continue
		}
		access = append(access, SkillAccess{
			SkillID:  strings.TrimSpace(s.ID),
			Name:     strings.TrimSpace(s.Name),
			Branch:   branch,
			CloneURL: cloneURL,
			RepoID:   strings.TrimSpace(repoIDOf(s.Repository)),
		})
	}
	if len(access) == 0 {
		// Server reached, response decoded, agent has no accessible skills.
		// This is authoritative — callers may safely clear caches.
		slog.InfoContext(ctx, "skillprovider: agent has no accessible skills",
			"agent_id", agentID)
		return nil, LookupUnbound, nil
	}
	lease, err := gitcred.ParseTokenLease(batchResp.SyncToken, batchResp.SyncTokenExpires)
	if err != nil {
		return nil, LookupUnknown, fmt.Errorf("skillprovider: sync lease for agent %s: %w", agentID, err)
	}
	auth := gitcred.NewRefreshableLeaseAuth(lease, func(refreshCtx context.Context) (gitcred.TokenLease, error) {
		return p.refreshSyncLease(refreshCtx, agentID)
	})
	for i := range access {
		access[i].Auth = auth
	}
	slog.InfoContext(ctx, "skillprovider: resolved skill bindings",
		"agent_id", agentID, "count", len(access))
	return access, LookupBound, nil
}

type batchLookup struct {
	serverURL   string
	workspaceID string
	endpoint    string
	response    batchResponse
}

func (p *Provider) lookupBatch(ctx context.Context, agentID string) (batchLookup, error) {
	var batchResp batchResponse
	conn, ok := p.resolver.ResolveServerForAgent(ctx, agentID)
	if !ok {
		// Resolver failure: channel not running, agent not in profile, or token
		// unresolvable. None of these mean the agent has lost its bindings.
		return batchLookup{}, fmt.Errorf("skillprovider: resolve channel for agent %s: unavailable", agentID)
	}
	endpoint, err := asset.PostAgentsBatchAccess(ctx, p.httpClient, conn, "/api/v1/skills/agents/access:batch", []string{agentID}, &batchResp)
	if err != nil {
		return batchLookup{}, fmt.Errorf("skillprovider: %w", err)
	}

	return batchLookup{serverURL: strings.TrimRight(strings.TrimSpace(conn.ServerURL), "/"), workspaceID: strings.TrimSpace(conn.WorkspaceID), endpoint: endpoint, response: batchResp}, nil
}

func (p *Provider) refreshSyncLease(ctx context.Context, agentID string) (gitcred.TokenLease, error) {
	agentID = strings.TrimSpace(agentID)
	lookup, err := p.lookupBatch(ctx, agentID)
	if err != nil {
		return gitcred.TokenLease{}, err
	}
	rawSkills, ok := lookup.response.Agents[agentID]
	if !ok {
		return gitcred.TokenLease{}, fmt.Errorf("skillprovider: refresh response omitted requested agent %s", agentID)
	}
	if len(rawSkills) == 0 {
		return gitcred.TokenLease{}, fmt.Errorf("skillprovider: skill binding for agent %s is no longer bound", agentID)
	}
	return gitcred.ParseTokenLease(lookup.response.SyncToken, lookup.response.SyncTokenExpires)
}

// composeCloneURL builds the git clone URL from the canonical repository full
// name returned by MeshX.
func composeCloneURL(serverURL, workspaceID string, repo *repositoryRef) string {
	if repo == nil {
		return ""
	}
	fullName := strings.TrimSpace(repo.RepoFullName)
	if fullName == "" {
		return ""
	}
	return asset.ComposeCloneURL(serverURL, workspaceID, fullName)
}

// repoIDOf safely extracts the AGS repository id from a possibly-nil ref.
func repoIDOf(repo *repositoryRef) string {
	if repo == nil {
		return ""
	}
	return repo.RepoID
}

// AgentServerConnectionResolver resolves agent→channel using profile agent config
// (Source.ID) and the running server runtime manager. It implements ServerConnectionResolver.
type AgentServerConnectionResolver struct {
	serverConnections ServerConnectionAccessor
}

// ServerConnectionAccessor exposes the channel manager's lookup.
type ServerConnectionAccessor interface {
	ServerConnection(id string) (types.ServerConnection, bool)
}

// ConnectionInfoProvider is satisfied by *wire.Protocol.
type ConnectionInfoProvider interface {
	ServerURL() string
	WorkspaceID() string
	UserToken(ctx context.Context) (string, error)
	RefreshUserToken(ctx context.Context, staleToken string) (string, error)
}

// NewAgentServerConnectionResolver builds a resolver backed by the given channel
// accessor.
func NewAgentServerConnectionResolver(serverConnections ServerConnectionAccessor) *AgentServerConnectionResolver {
	return &AgentServerConnectionResolver{serverConnections: serverConnections}
}

// ResolveServerForAgent returns connection info for the AgentTeam server runtime that
// owns agentID. Returns false when the agent has no AgentTeam source, the channel
// is not running, or the user token cannot be obtained.
func (r *AgentServerConnectionResolver) ResolveServerForAgent(ctx context.Context, agentID string) (asset.ConnectionInfo, bool) {
	ag, ok := profile.Default().Agents().Get(agentID)
	if !ok || ag.Source.Type != "agentteam" || strings.TrimSpace(ag.Source.ID) == "" {
		return asset.ConnectionInfo{}, false
	}
	ch, ok := r.serverConnections.ServerConnection(ag.Source.ID)
	if !ok {
		return asset.ConnectionInfo{}, false
	}
	conn, ok := ch.(ConnectionInfoProvider)
	if !ok {
		return asset.ConnectionInfo{}, false
	}
	token, err := conn.UserToken(ctx)
	if err != nil {
		slog.WarnContext(ctx, "skillprovider: failed to resolve user token",
			"agent_id", agentID, "error", err)
		return asset.ConnectionInfo{}, false
	}
	return asset.ConnectionInfo{
		ServerURL:        conn.ServerURL(),
		UserToken:        token,
		RefreshUserToken: conn.RefreshUserToken,
		WorkspaceID:      conn.WorkspaceID(),
		TemplateLocale:   profile.EffectiveLanguage(profile.Default().Config()),
		HTTPClient:       nil,
	}, true
}
