//go:build !android && !ios && !freebsd && !js

package services

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"os/user"
	"runtime"

	log "github.com/sirupsen/logrus"

	"github.com/netbirdio/netbird/client/internal/profilemanager"
	"github.com/netbirdio/netbird/client/proto"
)

// LoginParams are the inputs to Login.
type LoginParams struct {
	ProfileName   string `json:"profileName"`
	Username      string `json:"username"`
	ManagementURL string `json:"managementUrl"`
	SetupKey      string `json:"setupKey"`
	PreSharedKey  string `json:"preSharedKey"`
	Hostname      string `json:"hostname"`
	Hint          string `json:"hint"`
}

// LoginResult is the daemon's reply to Login.
type LoginResult struct {
	NeedsSSOLogin           bool   `json:"needsSsoLogin"`
	UserCode                string `json:"userCode"`
	VerificationURI         string `json:"verificationUri"`
	VerificationURIComplete string `json:"verificationUriComplete"`
	// ProfileID is the ID of the profile this login ran against, or "" when the
	// caller named the profile itself and no ID was resolved. Pass it back in
	// WaitSSOParams so the account email lands on this profile even if the
	// active one changes during SSO.
	ProfileID string `json:"profileId"`
}

// WaitSSOParams are the inputs to waitSSOLogin.
type WaitSSOParams struct {
	UserCode string `json:"userCode"`
	Hostname string `json:"hostname"`
	// ProfileID is the profile the login was started for, used to file the
	// account email against it rather than against whichever profile is active
	// when the flow returns. Optional: empty falls back to the active profile.
	ProfileID string `json:"profileId"`
}

// UpParams selects the profile to bring up.
type UpParams struct {
	ProfileName string `json:"profileName"`
	Username    string `json:"username"`
}

// LogoutParams selects the profile to log out.
type LogoutParams struct {
	ProfileName string `json:"profileName"`
	Username    string `json:"username"`
}

// Connection groups the daemon RPCs that drive login / connect / disconnect.
type Connection struct {
	conn       DaemonConn
	classifier errorClassifier
}

// NewConnection wires up a Connection. translator or prefs may be nil, in which
// case classifyDaemonError falls back to the bare error key.
func NewConnection(conn DaemonConn, translator ErrorTranslator, prefs LanguagePreference) *Connection {
	return &Connection{conn: conn, classifier: errorClassifier{translator: translator, prefs: prefs}}
}

func (s *Connection) Login(ctx context.Context, p LoginParams) (LoginResult, error) {
	cli, err := s.conn.Client()
	if err != nil {
		return LoginResult{}, err
	}

	// No pre-Login Down: Login dislodges a pending WaitSSOLogin itself, and a
	// defensive Down would only flash an Idle blink in the tray during handoff.

	// Fall back to the daemon's active profile and the current OS user.
	profileName := p.ProfileName
	username := p.Username
	// Only set when the daemon told us the ID. A caller-supplied ProfileName is
	// a handle — a display name or an ID prefix resolve too — and the state file
	// is named after the ID, so passing a handle on would name the wrong file.
	profileID := ""
	if profileName == "" {
		if active, aerr := cli.GetActiveProfile(ctx, &proto.GetActiveProfileRequest{}); aerr == nil {
			// Address the active profile by ID (the daemon resolves it as a
			// handle); names can collide, the ID cannot.
			profileName = active.GetId()
			profileID = profileName
			if username == "" {
				username = active.GetUsername()
			}
		}
	}
	if username == "" {
		if u, uerr := user.Current(); uerr == nil {
			username = u.Username
		}
	}

	req := &proto.LoginRequest{
		ManagementUrl:       p.ManagementURL,
		SetupKey:            p.SetupKey,
		Hostname:            p.Hostname,
		IsUnixDesktopClient: runtime.GOOS == "linux",
	}
	if profileName != "" {
		req.ProfileName = ptrStr(profileName)
	}
	if username != "" {
		req.Username = ptrStr(username)
	}
	if p.PreSharedKey != "" {
		req.OptionalPreSharedKey = ptrStr(p.PreSharedKey)
	}
	if p.Hint != "" {
		req.Hint = ptrStr(p.Hint)
	}

	resp, err := cli.Login(ctx, req)
	if err != nil {
		return LoginResult{}, s.classifyDaemonError(err)
	}
	log.Infof("daemon login response received, needs SSO login: %v", resp.GetNeedsSSOLogin())
	return LoginResult{
		NeedsSSOLogin:           resp.GetNeedsSSOLogin(),
		UserCode:                resp.GetUserCode(),
		VerificationURI:         resp.GetVerificationURI(),
		VerificationURIComplete: resp.GetVerificationURIComplete(),
		ProfileID:               profileID,
	}, nil
}

func (s *Connection) Up(ctx context.Context, p UpParams) error {
	cli, err := s.conn.Client()
	if err != nil {
		return err
	}
	log.Infof("sending up request to daemon")
	// Always async: status updates flow via SubscribeStatus.
	req := &proto.UpRequest{Async: true}
	if p.ProfileName != "" {
		req.ProfileName = ptrStr(p.ProfileName)
	}
	if p.Username != "" {
		req.Username = ptrStr(p.Username)
	}
	if _, err = cli.Up(ctx, req); err != nil {
		return s.classifyDaemonError(err)
	}
	return nil
}

// WaitSSOLoginAndUp blocks until the SSO login completes and then brings the
// connection up, both from the Go side. Keeping the post-login Up here rather
// than as a frontend continuation is deliberate: during SSO the tray window is
// hidden and the webview is suspended (macOS App Nap / hidden-window timer
// throttling), so a frontend-driven Up would not run until the user woke the
// window (e.g. by hovering the tray icon). Doing it in Go connects the moment
// the daemon reports SSO success. Returns the authenticated user's email.
func (s *Connection) WaitSSOLoginAndUp(ctx context.Context, wait WaitSSOParams, up UpParams) (string, error) {
	email, err := s.waitSSOLogin(ctx, wait)
	if err != nil {
		return "", err
	}
	if err := ctx.Err(); err != nil {
		return "", err
	}
	if err := s.Up(ctx, up); err != nil {
		return "", err
	}
	return email, nil
}

func (s *Connection) Down(ctx context.Context) error {
	cli, err := s.conn.Client()
	if err != nil {
		return err
	}
	if _, err = cli.Down(ctx, &proto.DownRequest{}); err != nil {
		return s.classifyDaemonError(err)
	}
	return nil
}

// OpenURL opens url in an external browser; the embedded webview blocks
// window.open, so the SSO verification page can't pop inline. Honors $BROWSER
// before the platform default.
func (s *Connection) OpenURL(url string) error {
	if browser := os.Getenv("BROWSER"); browser != "" {
		return exec.Command(browser, url).Start()
	}
	switch runtime.GOOS {
	case "windows":
		return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
	case "darwin":
		return exec.Command("open", url).Start()
	case "linux":
		return exec.Command("xdg-open", url).Start()
	default:
		return fmt.Errorf("unsupported platform")
	}
}

func (s *Connection) Logout(ctx context.Context, p LogoutParams) error {
	cli, err := s.conn.Client()
	if err != nil {
		return err
	}
	req := &proto.LogoutRequest{}
	if p.ProfileName != "" {
		req.ProfileName = ptrStr(p.ProfileName)
	}
	if p.Username != "" {
		req.Username = ptrStr(p.Username)
	}
	if _, err = cli.Logout(ctx, req); err != nil {
		return s.classifyDaemonError(err)
	}

	// The daemon runs as root and can't reach the user-owned per-profile state
	// file holding the account email (see Profiles.List), so clear the stale
	// email here; the next SSO login recreates it.
	if p.ProfileName != "" {
		if err := profilemanager.NewProfileManager().RemoveProfileState(p.ProfileName); err != nil {
			// Non-fatal: the logout itself succeeded.
			log.Warnf("failed to remove profile state for %s: %v", p.ProfileName, err)
		}
	}

	return nil
}

// waitSSOLogin blocks until the daemon reports the SSO login result and returns
// the authenticated user's email. It is unexported because the frontend drives
// SSO through the exported WaitSSOLoginAndUp.
func (s *Connection) waitSSOLogin(ctx context.Context, p WaitSSOParams) (string, error) {
	cli, err := s.conn.Client()
	if err != nil {
		return "", err
	}
	log.Infof("waiting for SSO login to complete")
	resp, err := cli.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{
		UserCode: p.UserCode,
		Hostname: p.Hostname,
	})
	if err != nil {
		return "", s.classifyDaemonError(err)
	}
	log.Infof("SSO login completed, daemon reported success")

	// Persist the account email the same way the CLI does after its own
	// WaitSSOLogin: the daemon returns it but cannot store it, since it runs as
	// root and the per-profile state file is user-owned (see Logout below).
	// Without this the profile has no email, so Profiles.List shows no account
	// and later logins and session extends go out without a login_hint —
	// leaving the IdP to guess which account was meant.
	if email := resp.GetEmail(); email != "" {
		state := &profilemanager.ProfileState{Email: email}
		pm := profilemanager.NewProfileManager()

		// Against the profile the login was started for: SSO spans seconds of
		// user interaction, and a profile switch in that window would otherwise
		// file the email under the wrong profile.
		if p.ProfileID != "" {
			err = pm.SetProfileState(profilemanager.ID(p.ProfileID), state)
		} else {
			err = pm.SetActiveProfileState(state)
		}
		if err != nil {
			// Non-fatal: the login itself succeeded.
			log.Warnf("failed to store account email: %v", err)
		}
	}

	return resp.GetEmail(), nil
}

// classifyDaemonError maps a gRPC error to a localised ClientError.
func (s *Connection) classifyDaemonError(err error) *ClientError {
	return s.classifier.classify(err)
}
