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

package services

import (
	"context"
	"os/user"

	log "github.com/sirupsen/logrus"

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

type Profile struct {
	// ID is the daemon-generated on-disk identity of the profile. Display
	// names can collide and be renamed, so the ID is the stable handle the
	// daemon resolves switch/remove/logout requests against.
	ID       string `json:"id"`
	Name     string `json:"name"`
	IsActive bool   `json:"isActive"`
	// Email is read from the user-owned per-profile state file (CLI writes it
	// after SSO login), not via ListProfiles: the daemon runs as root and can't
	// reach it, while the UI runs as the logged-in user.
	Email string `json:"email"`
}

type ProfileRef struct {
	ProfileName string `json:"profileName"`
	Username    string `json:"username"`
}

type ActiveProfile struct {
	// ID is the active profile's stable on-disk identity. Use it (not the
	// display name) as the handle for daemon requests and active-profile
	// comparisons, since names can collide.
	ID          string `json:"id"`
	ProfileName string `json:"profileName"`
	Username    string `json:"username"`
}

// RenameProfileParams selects a profile by handle and carries its new display
// name.
type RenameProfileParams struct {
	// Handle selects the profile to rename: an exact ID, a unique ID prefix,
	// or a unique display name. The daemon resolves it server-side.
	Handle string `json:"handle"`
	// NewName is the new free-form display name. The daemon sanitizes it
	// (strips control characters, trims, caps length) but keeps spaces, emoji,
	// punctuation, and non-ASCII letters.
	NewName string `json:"newName"`

	Username string `json:"username"`
}

type Profiles struct {
	conn DaemonConn
}

func NewProfiles(conn DaemonConn) *Profiles {
	return &Profiles{conn: conn}
}

// Username returns the OS username the daemon expects for profile lookups.
func (s *Profiles) Username() (string, error) {
	u, err := user.Current()
	if err != nil {
		return "", err
	}
	return u.Username, nil
}

func (s *Profiles) List(ctx context.Context, username string) ([]Profile, error) {
	cli, err := s.conn.Client()
	if err != nil {
		return nil, err
	}
	resp, err := cli.ListProfiles(ctx, &proto.ListProfilesRequest{Username: username})
	if err != nil {
		return nil, err
	}
	pm := profilemanager.NewProfileManager()
	out := make([]Profile, 0, len(resp.GetProfiles()))
	for _, p := range resp.GetProfiles() {
		prof := Profile{ID: p.GetId(), Name: p.GetName(), IsActive: p.GetIsActive()}
		if state, err := pm.GetProfileState(profilemanager.ID(p.GetId())); err == nil {
			prof.Email = state.Email
		}
		out = append(out, prof)
	}
	return out, nil
}

func (s *Profiles) GetActive(ctx context.Context) (ActiveProfile, error) {
	cli, err := s.conn.Client()
	if err != nil {
		return ActiveProfile{}, err
	}
	resp, err := cli.GetActiveProfile(ctx, &proto.GetActiveProfileRequest{})
	if err != nil {
		return ActiveProfile{}, err
	}
	return ActiveProfile{
		ID:          resp.GetId(),
		ProfileName: resp.GetProfileName(),
		Username:    resp.GetUsername(),
	}, nil
}

// Switch sends a profile switch to the daemon and returns the resolved
// on-disk ID of the now-active profile. ProfileName is treated as a handle
// (exact ID, unique ID prefix, or unique display name); the daemon resolves
// it server-side and echoes back the canonical ID.
func (s *Profiles) Switch(ctx context.Context, p ProfileRef) (string, error) {
	cli, err := s.conn.Client()
	if err != nil {
		return "", err
	}
	req := &proto.SwitchProfileRequest{}
	if p.ProfileName != "" {
		req.ProfileName = ptrStr(p.ProfileName)
	}
	if p.Username != "" {
		req.Username = ptrStr(p.Username)
	}
	resp, err := cli.SwitchProfile(ctx, req)
	if err != nil {
		return "", err
	}
	return resp.GetId(), nil
}

// Add creates a profile with the given display name and returns its
// daemon-generated on-disk ID, so callers can address the new profile by ID
// (e.g. to write config or switch to it) without re-resolving the name.
func (s *Profiles) Add(ctx context.Context, p ProfileRef) (string, error) {
	cli, err := s.conn.Client()
	if err != nil {
		return "", err
	}
	resp, err := cli.AddProfile(ctx, &proto.AddProfileRequest{
		ProfileName: p.ProfileName,
		Username:    p.Username,
	})
	if err != nil {
		return "", err
	}
	return resp.GetId(), nil
}

func (s *Profiles) Remove(ctx context.Context, p ProfileRef) error {
	cli, err := s.conn.Client()
	if err != nil {
		return err
	}
	resp, err := cli.RemoveProfile(ctx, &proto.RemoveProfileRequest{
		ProfileName: p.ProfileName,
		Username:    p.Username,
	})
	if err != nil {
		return err
	}

	// The daemon deletes what it owns but runs as root, so it leaves the
	// user-owned state file holding the account email behind (same split as
	// Connection.Logout). Legacy profiles are keyed by name rather than by a
	// generated ID, so a recreated profile of the same name would inherit the
	// deleted one's email and offer it as the login_hint.
	//
	// Keyed on the ID the daemon resolved, not on the request handle: that may
	// have been a display name or an ID prefix, which would name a different
	// file (or none).
	if id := resp.GetId(); id != "" {
		if err := profilemanager.NewProfileManager().RemoveProfileState(id); err != nil {
			// Non-fatal: the profile itself is gone.
			log.Warnf("failed to remove profile state for %s: %v", id, err)
		}
	}

	return nil
}

// Rename changes a profile's display name. The on-disk ID is unaffected, so
// the active profile and any ID-based references stay valid (the default
// profile can be renamed too — only its display name changes). Returns the
// profile's previous display name as confirmation.
func (s *Profiles) Rename(ctx context.Context, p RenameProfileParams) (string, error) {
	cli, err := s.conn.Client()
	if err != nil {
		return "", err
	}
	resp, err := cli.RenameProfile(ctx, &proto.RenameProfileRequest{
		Username:       p.Username,
		Handle:         p.Handle,
		NewProfileName: p.NewName,
	})
	if err != nil {
		return "", err
	}
	return resp.GetOldProfileName(), nil
}
