package config

import (
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"sort"
)

const authStateVersion = 1

// authConfig implements AuthConfig interface
type authConfig struct {
	config *config
}

type authState struct {
	Version     int                   `json:"version"`
	DefaultHost string                `json:"default_host,omitempty"`
	Hosts       map[string]*hostState `json:"hosts,omitempty"`
}

type hostState struct {
	ActiveUser string                `json:"active_user,omitempty"`
	Users      map[string]*userState `json:"users,omitempty"`
}

type userState struct {
	Token       string `json:"token"`
	GitProtocol string `json:"git_protocol,omitempty"`
}

// ActiveToken returns the active token for a host
func (a *authConfig) ActiveToken(hostname string) (string, string) {
	if token := os.Getenv("GC_TOKEN"); token != "" {
		return token, "GC_TOKEN"
	}
	if token := os.Getenv("GITCODE_TOKEN"); token != "" {
		return token, "GITCODE_TOKEN"
	}

	return a.StoredToken(hostname)
}

// StoredToken returns the stored token for a host without environment overrides.
func (a *authConfig) StoredToken(hostname string) (string, string) {
	state, err := a.config.readAuthState()
	if err != nil {
		return "", ""
	}
	user := state.activeUser(hostname)
	if user == nil || user.Token == "" {
		return "", ""
	}
	return user.Token, "config"
}

// HasActiveToken checks if a token exists for a host
func (a *authConfig) HasActiveToken(hostname string) bool {
	token, _ := a.ActiveToken(hostname)
	return token != ""
}

// ActiveUser returns the active user for a host
func (a *authConfig) ActiveUser(hostname string) (string, error) {
	state, err := a.config.readAuthState()
	if err != nil {
		return "", err
	}

	host := state.host(hostname)
	if host == nil {
		return "", nil
	}
	return host.ActiveUser, nil
}

// Hosts returns all configured hosts
func (a *authConfig) Hosts() []string {
	state, err := a.config.readAuthState()
	if err != nil {
		return []string{"gitcode.com"}
	}

	hosts := map[string]struct{}{"gitcode.com": {}}
	if host := getEnvHost(); host != "" {
		hosts[host] = struct{}{}
	}
	for host := range state.Hosts {
		hosts[host] = struct{}{}
	}

	result := make([]string, 0, len(hosts))
	for host := range hosts {
		result = append(result, host)
	}
	sort.Strings(result)
	return result
}

// DefaultHost returns the default host
func (a *authConfig) DefaultHost() (string, string) {
	if host := getEnvHost(); host != "" {
		return host, "environment"
	}

	state, err := a.config.readAuthState()
	if err == nil && state.DefaultHost != "" {
		return state.DefaultHost, "config"
	}
	return "gitcode.com", "default"
}

// Login creates or updates authentication for a host
func (a *authConfig) Login(hostname, username, token, gitProtocol string, secureStorage bool) (bool, error) {
	if hostname == "" {
		hostname = "gitcode.com"
	}
	if username == "" || token == "" {
		return false, errors.New("username and token are required")
	}
	if secureStorage {
		return false, errors.New("secure storage is not supported yet")
	}
	if gitProtocol == "" {
		gitProtocol = "https"
	}

	state, err := a.config.readAuthState()
	if err != nil {
		return false, err
	}

	host := state.ensureHost(hostname)
	user := host.Users[username]
	changed := state.DefaultHost != hostname || host.ActiveUser != username
	if user == nil {
		user = &userState{}
		host.Users[username] = user
		changed = true
	}
	if user.Token != token || user.GitProtocol != gitProtocol {
		changed = true
	}

	user.Token = token
	user.GitProtocol = gitProtocol
	host.ActiveUser = username
	state.DefaultHost = hostname

	if !changed {
		return false, nil
	}

	return true, a.config.writeAuthState(state)
}

// Logout removes authentication for a host
func (a *authConfig) Logout(hostname, username string) error {
	if hostname == "" {
		hostname = "gitcode.com"
	}

	state, err := a.config.readAuthState()
	if err != nil {
		return err
	}

	host := state.host(hostname)
	if host == nil {
		return nil
	}

	if username == "" {
		username = host.ActiveUser
	}
	if username == "" {
		return nil
	}

	delete(host.Users, username)
	if len(host.Users) == 0 {
		delete(state.Hosts, hostname)
		if state.DefaultHost == hostname {
			state.DefaultHost = ""
		}
		return a.config.writeAuthState(state)
	}

	if host.ActiveUser == username {
		host.ActiveUser = firstUsername(host.Users)
	}

	return a.config.writeAuthState(state)
}

// SwitchUser switches the active user for a host
func (a *authConfig) SwitchUser(hostname, user string) error {
	if hostname == "" {
		hostname = "gitcode.com"
	}

	state, err := a.config.readAuthState()
	if err != nil {
		return err
	}

	host := state.host(hostname)
	if host == nil {
		return errors.New("host not found")
	}
	if _, ok := host.Users[user]; !ok {
		return errors.New("user not found")
	}

	host.ActiveUser = user
	state.DefaultHost = hostname
	return a.config.writeAuthState(state)
}

func (c *config) authStatePath() string {
	return filepath.Join(c.configDir, "auth.json")
}

func (c *config) readAuthState() (*authState, error) {
	path := c.authStatePath()
	data, err := os.ReadFile(path)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return &authState{
				Version: authStateVersion,
				Hosts:   map[string]*hostState{},
			}, nil
		}
		return nil, err
	}

	var state authState
	if err := json.Unmarshal(data, &state); err != nil {
		return nil, err
	}
	if state.Version == 0 {
		state.Version = authStateVersion
	}
	if state.Hosts == nil {
		state.Hosts = map[string]*hostState{}
	}
	for _, host := range state.Hosts {
		if host.Users == nil {
			host.Users = map[string]*userState{}
		}
	}
	return &state, nil
}

func (c *config) writeAuthState(state *authState) error {
	if state == nil {
		state = &authState{
			Version: authStateVersion,
			Hosts:   map[string]*hostState{},
		}
	}
	if state.Hosts == nil {
		state.Hosts = map[string]*hostState{}
	}
	state.Version = authStateVersion

	if err := os.MkdirAll(c.configDir, 0o700); err != nil {
		return err
	}
	if err := os.Chmod(c.configDir, 0o700); err != nil {
		return err
	}

	data, err := json.MarshalIndent(state, "", "  ")
	if err != nil {
		return err
	}
	return secureWriteFile(c.authStatePath(), data, 0o600)
}

func (s *authState) host(hostname string) *hostState {
	if hostname == "" {
		hostname = s.DefaultHost
	}
	if hostname == "" {
		hostname = "gitcode.com"
	}
	return s.Hosts[hostname]
}

func (s *authState) ensureHost(hostname string) *hostState {
	if s.Hosts == nil {
		s.Hosts = map[string]*hostState{}
	}
	host := s.host(hostname)
	if host != nil {
		return host
	}
	host = &hostState{Users: map[string]*userState{}}
	s.Hosts[hostname] = host
	return host
}

func (s *authState) activeUser(hostname string) *userState {
	host := s.host(hostname)
	if host == nil || host.ActiveUser == "" {
		return nil
	}
	return host.Users[host.ActiveUser]
}

func firstUsername(users map[string]*userState) string {
	names := make([]string, 0, len(users))
	for name := range users {
		names = append(names, name)
	}
	sort.Strings(names)
	if len(names) == 0 {
		return ""
	}
	return names[0]
}

func getEnvHost() string {
	return os.Getenv("GC_HOST")
}