package instance

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/mail"
	"strings"
	"sync"
	"time"

	"github.com/dexidp/dex/storage"
	goversion "github.com/hashicorp/go-version"
	log "github.com/sirupsen/logrus"

	"github.com/netbirdio/netbird/management/server/idp"
	"github.com/netbirdio/netbird/management/server/store"
	"github.com/netbirdio/netbird/shared/management/status"
	"github.com/netbirdio/netbird/version"
)

const (
	// Version endpoints
	managementVersionURL = "https://pkgs.netbird.io/releases/latest/version"
	dashboardReleasesURL = "https://api.github.com/repos/netbirdio/dashboard/releases/latest"

	// Cache TTL for version information
	versionCacheTTL = 60 * time.Minute

	// HTTP client timeout
	httpTimeout = 5 * time.Second
)

// VersionInfo contains version information for NetBird components
type VersionInfo struct {
	// CurrentVersion is the running management server version
	CurrentVersion string
	// DashboardVersion is the latest available dashboard version from GitHub
	DashboardVersion string
	// ManagementVersion is the latest available management version from GitHub
	ManagementVersion string
	// ManagementUpdateAvailable indicates if a newer management version is available
	ManagementUpdateAvailable bool
}

// githubRelease represents a GitHub release response
type githubRelease struct {
	TagName string `json:"tag_name"`
}

// Manager handles instance-level operations like initial setup.
type Manager interface {
	// IsSetupRequired checks if instance setup is required.
	// Returns true if embedded IDP is enabled and no accounts exist.
	IsSetupRequired(ctx context.Context) (bool, error)

	// CreateOwnerUser creates the initial owner user in the embedded IDP.
	// This should only be called when IsSetupRequired returns true.
	CreateOwnerUser(ctx context.Context, email, password, name string) (*idp.UserData, error)

	// RollbackSetup reverses a successful CreateOwnerUser by deleting the user
	// from the embedded IDP and reloading setupRequired from persistent state, so
	// /api/setup can be retried only when no accounts or local users remain. Used
	// when post-user steps (account or PAT creation) fail and the caller wants a
	// clean slate.
	RollbackSetup(ctx context.Context, userID string) error

	// GetVersionInfo returns version information for NetBird components.
	GetVersionInfo(ctx context.Context) (*VersionInfo, error)
}

type instanceStore interface {
	GetAccountsCounter(ctx context.Context) (int64, error)
}

type embeddedIdP interface {
	CreateUserWithPassword(ctx context.Context, email, password, name string) (*idp.UserData, error)
	DeleteUser(ctx context.Context, userID string) error
	GetAllAccounts(ctx context.Context) (map[string][]*idp.UserData, error)
}

// DefaultManager is the default implementation of Manager.
type DefaultManager struct {
	store              instanceStore
	embeddedIdpManager embeddedIdP

	setupRequired bool
	setupMu       sync.RWMutex

	// Version caching
	httpClient       *http.Client
	versionMu        sync.RWMutex
	cachedVersions   *VersionInfo
	lastVersionFetch time.Time
}

// NewManager creates a new instance manager.
// If idpManager is not an EmbeddedIdPManager, setup-related operations will return appropriate defaults.
func NewManager(ctx context.Context, store store.Store, idpManager idp.Manager) (Manager, error) {
	embeddedIdp, ok := idpManager.(*idp.EmbeddedIdPManager)

	m := &DefaultManager{
		store:         store,
		setupRequired: false,
		httpClient: &http.Client{
			Timeout: httpTimeout,
		},
	}

	if ok && embeddedIdp != nil {
		m.embeddedIdpManager = embeddedIdp
		err := m.loadSetupRequired(ctx)
		if err != nil {
			return nil, err
		}
	}

	return m, nil
}

func (m *DefaultManager) loadSetupRequired(ctx context.Context) error {
	// Check if there are any accounts in the NetBird store
	numAccounts, err := m.store.GetAccountsCounter(ctx)
	if err != nil {
		return err
	}
	hasAccounts := numAccounts > 0

	// Check if there are any users in the embedded IdP (Dex)
	users, err := m.embeddedIdpManager.GetAllAccounts(ctx)
	if err != nil {
		return err
	}
	hasLocalUsers := len(users) > 0

	m.setupMu.Lock()
	m.setupRequired = !(hasAccounts || hasLocalUsers)
	m.setupMu.Unlock()

	return nil
}

// IsSetupRequired checks if instance setup is required.
// Setup is required when:
// 1. Embedded IDP is enabled
// 2. No accounts exist in the store
func (m *DefaultManager) IsSetupRequired(_ context.Context) (bool, error) {
	if m.embeddedIdpManager == nil {
		return false, nil
	}

	m.setupMu.RLock()
	defer m.setupMu.RUnlock()

	return m.setupRequired, nil
}

// CreateOwnerUser creates the initial owner user in the embedded IDP.
func (m *DefaultManager) CreateOwnerUser(ctx context.Context, email, password, name string) (*idp.UserData, error) {

	if m.embeddedIdpManager == nil {
		return nil, errors.New("embedded IDP is not enabled")
	}

	if err := m.validateSetupInfo(email, password, name); err != nil {
		return nil, err
	}

	m.setupMu.Lock()
	defer m.setupMu.Unlock()

	if !m.setupRequired {
		return nil, status.Errorf(status.PreconditionFailed, "setup already completed")
	}

	if err := m.checkSetupRequiredFromDB(ctx); err != nil {
		var sErr *status.Error
		if errors.As(err, &sErr) && sErr.Type() == status.PreconditionFailed {
			m.setupRequired = false
		}
		return nil, err
	}

	userData, err := m.embeddedIdpManager.CreateUserWithPassword(ctx, email, password, name)
	if err != nil {
		return nil, fmt.Errorf("failed to create user in embedded IdP: %w", err)
	}

	m.setupRequired = false

	log.WithContext(ctx).Infof("created owner user %s in embedded IdP", email)

	return userData, nil
}

// RollbackSetup undoes a successful CreateOwnerUser: deletes the user from the
// embedded IDP and reloads setupRequired from persistent state.
func (m *DefaultManager) RollbackSetup(ctx context.Context, userID string) error {
	if m.embeddedIdpManager == nil {
		return errors.New("embedded IDP is not enabled")
	}

	var deleteErr error
	if err := m.embeddedIdpManager.DeleteUser(ctx, userID); err != nil {
		if isNotFoundError(err) {
			log.WithContext(ctx).Debugf("setup rollback user %s already deleted", userID)
		} else {
			deleteErr = fmt.Errorf("failed to delete user from embedded IdP: %w", err)
		}
	}

	if err := m.loadSetupRequired(ctx); err != nil {
		reloadErr := fmt.Errorf("failed to reload setup state after rollback: %w", err)
		if deleteErr != nil {
			return errors.Join(deleteErr, reloadErr)
		}
		return reloadErr
	}

	if deleteErr != nil {
		return deleteErr
	}

	log.WithContext(ctx).Infof("rolled back setup for user %s", userID)
	return nil
}

func isNotFoundError(err error) bool {
	if err == nil {
		return false
	}
	if errors.Is(err, storage.ErrNotFound) {
		return true
	}
	if s, ok := status.FromError(err); ok {
		return s.Type() == status.NotFound
	}
	return false
}

func (m *DefaultManager) checkSetupRequiredFromDB(ctx context.Context) error {
	numAccounts, err := m.store.GetAccountsCounter(ctx)
	if err != nil {
		return fmt.Errorf("failed to check accounts: %w", err)
	}
	if numAccounts > 0 {
		return status.Errorf(status.PreconditionFailed, "setup already completed")
	}

	users, err := m.embeddedIdpManager.GetAllAccounts(ctx)
	if err != nil {
		return fmt.Errorf("failed to check IdP users: %w", err)
	}
	if len(users) > 0 {
		return status.Errorf(status.PreconditionFailed, "setup already completed")
	}

	return nil
}

func (m *DefaultManager) validateSetupInfo(email, password, name string) error {
	if email == "" {
		return status.Errorf(status.InvalidArgument, "email is required")
	}
	if _, err := mail.ParseAddress(email); err != nil {
		return status.Errorf(status.InvalidArgument, "invalid email format")
	}
	if name == "" {
		return status.Errorf(status.InvalidArgument, "name is required")
	}
	if password == "" {
		return status.Errorf(status.InvalidArgument, "password is required")
	}
	if len(password) < 8 {
		return status.Errorf(status.InvalidArgument, "password must be at least 8 characters")
	}
	if len(password) > 72 {
		return status.Errorf(status.InvalidArgument, "password must be at most 72 characters")
	}
	return nil
}

// GetVersionInfo returns version information for NetBird components.
func (m *DefaultManager) GetVersionInfo(ctx context.Context) (*VersionInfo, error) {
	m.versionMu.RLock()
	if m.cachedVersions != nil && time.Since(m.lastVersionFetch) < versionCacheTTL {
		cached := *m.cachedVersions
		m.versionMu.RUnlock()
		return &cached, nil
	}
	m.versionMu.RUnlock()

	return m.fetchVersionInfo(ctx)
}

func (m *DefaultManager) fetchVersionInfo(ctx context.Context) (*VersionInfo, error) {
	m.versionMu.Lock()
	// Double-check after acquiring write lock
	if m.cachedVersions != nil && time.Since(m.lastVersionFetch) < versionCacheTTL {
		cached := *m.cachedVersions
		m.versionMu.Unlock()
		return &cached, nil
	}
	m.versionMu.Unlock()

	info := &VersionInfo{
		CurrentVersion: version.NetbirdVersion(),
	}

	// Fetch management version from pkgs.netbird.io (plain text)
	mgmtVersion, err := m.fetchPlainTextVersion(ctx, managementVersionURL)
	if err != nil {
		log.WithContext(ctx).Warnf("failed to fetch management version: %v", err)
	} else {
		info.ManagementVersion = mgmtVersion
		info.ManagementUpdateAvailable = isNewerVersion(info.CurrentVersion, mgmtVersion)
	}

	// Fetch dashboard version from GitHub
	dashVersion, err := m.fetchGitHubRelease(ctx, dashboardReleasesURL)
	if err != nil {
		log.WithContext(ctx).Warnf("failed to fetch dashboard version from GitHub: %v", err)
	} else {
		info.DashboardVersion = dashVersion
	}

	// Update cache
	m.versionMu.Lock()
	m.cachedVersions = info
	m.lastVersionFetch = time.Now()
	m.versionMu.Unlock()

	return info, nil
}

// isNewerVersion returns true if latestVersion is greater than currentVersion
func isNewerVersion(currentVersion, latestVersion string) bool {
	current, err := goversion.NewVersion(currentVersion)
	if err != nil {
		return false
	}

	latest, err := goversion.NewVersion(latestVersion)
	if err != nil {
		return false
	}

	return latest.GreaterThan(current)
}

func (m *DefaultManager) fetchPlainTextVersion(ctx context.Context, url string) (string, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return "", fmt.Errorf("create request: %w", err)
	}

	req.Header.Set("User-Agent", "NetBird-Management/"+version.NetbirdVersion())

	resp, err := m.httpClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("execute request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	body, err := io.ReadAll(io.LimitReader(resp.Body, 100))
	if err != nil {
		return "", fmt.Errorf("read response: %w", err)
	}

	return strings.TrimSpace(string(body)), nil
}

func (m *DefaultManager) fetchGitHubRelease(ctx context.Context, url string) (string, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return "", fmt.Errorf("create request: %w", err)
	}

	req.Header.Set("Accept", "application/vnd.github.v3+json")
	req.Header.Set("User-Agent", "NetBird-Management/"+version.NetbirdVersion())

	resp, err := m.httpClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("execute request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	var release githubRelease
	if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
		return "", fmt.Errorf("decode response: %w", err)
	}

	// Remove 'v' prefix if present
	tag := release.TagName
	if len(tag) > 0 && tag[0] == 'v' {
		tag = tag[1:]
	}

	return tag, nil
}
