package main

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"sync"
	"time"
)

const (
	// SSO endpoints
	issuerAuthorizeURL = "https://login.eveonline.com/v2/oauth/authorize"
	issuerTokenURL     = "https://login.eveonline.com/v2/oauth/token"

	// ESI base
	esiBase = "https://esi.evetech.net"
)

// ESISSO encapsulates a minimal PKCE SSO client and token store
type ESISSO struct {
	clientID    string
	redirectURI string
	scopes      []string

	state        string
	codeVerifier string
	codeChallenge string

	mu           sync.Mutex
	accessToken  string
	refreshToken string
	expiresAt    time.Time

	characterID   int64
	characterName string

	callbackOnce sync.Once
	server       *http.Server
}

func NewESISSO(clientID string, redirectURI string, scopes []string) *ESISSO {
	return &ESISSO{
		clientID:    clientID,
		redirectURI: redirectURI,
		scopes:      scopes,
	}
}

// BuildAuthorizeURL prepares state and PKCE challenge and returns the browser URL
func (s *ESISSO) BuildAuthorizeURL() (string, error) {
	if s.clientID == "" {
		return "", errors.New("EVE_SSO_CLIENT_ID not set")
	}
	verifier, challenge, err := generatePKCE()
	if err != nil {
		return "", err
	}
	s.codeVerifier = verifier
	s.codeChallenge = challenge
	s.state = randString(24)

	q := url.Values{}
	q.Set("response_type", "code")
	q.Set("client_id", s.clientID)
	q.Set("redirect_uri", s.redirectURI)
	if len(s.scopes) > 0 {
		q.Set("scope", strings.Join(s.scopes, " "))
	}
	q.Set("state", s.state)
	q.Set("code_challenge", s.codeChallenge)
	q.Set("code_challenge_method", "S256")

	return issuerAuthorizeURL + "?" + q.Encode(), nil
}

// StartCallbackServerAsync starts the callback server in the background and returns immediately
func (s *ESISSO) StartCallbackServerAsync() error {
	u, err := url.Parse(s.redirectURI)
	if err != nil {
		return err
	}
	if u.Scheme != "http" && u.Scheme != "https" {
		return errors.New("redirect URI must be http(s)")
	}
	hostPort := u.Host
	if !strings.Contains(hostPort, ":") {
		if u.Scheme == "https" {
			hostPort += ":443"
		} else {
			hostPort += ":80"
		}
	}

	mux := http.NewServeMux()
	mux.HandleFunc(u.Path, func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			w.WriteHeader(http.StatusMethodNotAllowed)
			return
		}
		q := r.URL.Query()
		code := q.Get("code")
		st := q.Get("state")
		if code == "" || st == "" || st != s.state {
			w.WriteHeader(http.StatusBadRequest)
			_, _ = w.Write([]byte("Invalid SSO response"))
			return
		}
		if err := s.exchangeToken(r.Context(), code); err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			_, _ = w.Write([]byte("Token exchange failed: " + err.Error()))
			return
		}
		_, _ = io.WriteString(w, "Login successful. You can close this window.")
		go func() {
			time.Sleep(200 * time.Millisecond)
			_ = s.server.Shutdown(context.Background())
		}()
	})

	ln, err := net.Listen("tcp", hostPort)
	if err != nil {
		return err
	}

	s.server = &http.Server{Handler: mux}
	go func() { _ = s.server.Serve(ln) }()
	return nil
}

// Deprecated: blocking start; prefer StartCallbackServerAsync
func (s *ESISSO) StartCallbackServer() error {
	u, err := url.Parse(s.redirectURI)
	if err != nil {
		return err
	}
	if u.Scheme != "http" && u.Scheme != "https" {
		return errors.New("redirect URI must be http(s)")
	}
	hostPort := u.Host
	if !strings.Contains(hostPort, ":") {
		if u.Scheme == "https" {
			hostPort += ":443"
		} else {
			hostPort += ":80"
		}
	}

	mux := http.NewServeMux()
	mux.HandleFunc(u.Path, func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			w.WriteHeader(http.StatusMethodNotAllowed)
			return
		}
		q := r.URL.Query()
		code := q.Get("code")
		st := q.Get("state")
		if code == "" || st == "" || st != s.state {
			w.WriteHeader(http.StatusBadRequest)
			_, _ = w.Write([]byte("Invalid SSO response"))
			return
		}
		if err := s.exchangeToken(r.Context(), code); err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			_, _ = w.Write([]byte("Token exchange failed: " + err.Error()))
			return
		}
		_, _ = io.WriteString(w, "Login successful. You can close this window.")
		go func() {
			time.Sleep(200 * time.Millisecond)
			_ = s.server.Shutdown(context.Background())
		}()
	})

	ln, err := net.Listen("tcp", hostPort)
	if err != nil {
		return err
	}

	s.server = &http.Server{Handler: mux}
	return s.server.Serve(ln)
}

func (s *ESISSO) exchangeToken(ctx context.Context, code string) error {
	form := url.Values{}
	form.Set("grant_type", "authorization_code")
	form.Set("code", code)
	form.Set("client_id", s.clientID)
	form.Set("code_verifier", s.codeVerifier)

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, issuerTokenURL, strings.NewReader(form.Encode()))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		b, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("token exchange failed: %s: %s", resp.Status, string(b))
	}
	var tr tokenResponse
	if err := json.NewDecoder(resp.Body).Decode(&tr); err != nil {
		return err
	}

	s.mu.Lock()
	defer s.mu.Unlock()
	if tr.AccessToken != "" {
		s.accessToken = tr.AccessToken
	}
	if tr.RefreshToken != "" {
		s.refreshToken = tr.RefreshToken
	}
	if tr.ExpiresIn > 0 {
		s.expiresAt = time.Now().Add(time.Duration(tr.ExpiresIn-30) * time.Second)
	}
	// Parse basic claims for display
	name, cid := parseTokenCharacter(tr.AccessToken)
	s.characterName = name
	s.characterID = cid
	return nil
}

func (s *ESISSO) refresh(ctx context.Context) error {
	s.mu.Lock()
	rt := s.refreshToken
	s.mu.Unlock()
	if rt == "" {
		return errors.New("no refresh token")
	}
	form := url.Values{}
	form.Set("grant_type", "refresh_token")
	form.Set("refresh_token", rt)
	form.Set("client_id", s.clientID)

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, issuerTokenURL, strings.NewReader(form.Encode()))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		b, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("token refresh failed: %s: %s", resp.Status, string(b))
	}
	var tr tokenResponse
	if err := json.NewDecoder(resp.Body).Decode(&tr); err != nil {
		return err
	}

	s.mu.Lock()
	defer s.mu.Unlock()
	s.accessToken = tr.AccessToken
	if tr.RefreshToken != "" {
		s.refreshToken = tr.RefreshToken
	}
	if tr.ExpiresIn > 0 {
		s.expiresAt = time.Now().Add(time.Duration(tr.ExpiresIn-30) * time.Second)
	}
	name, cid := parseTokenCharacter(tr.AccessToken)
	s.characterName = name
	s.characterID = cid
	return nil
}

func (s *ESISSO) ensureAccessToken(ctx context.Context) (string, error) {
	s.mu.Lock()
	tok := s.accessToken
	exp := s.expiresAt
	s.mu.Unlock()
	if tok == "" {
		return "", errors.New("not logged in")
	}
	if time.Now().After(exp.Add(-60 * time.Second)) {
		if err := s.refresh(ctx); err != nil {
			return "", err
		}
		s.mu.Lock()
		tok = s.accessToken
		s.mu.Unlock()
	}
	return tok, nil
}

// PostWaypoint calls ESI to set destination or add waypoint
func (s *ESISSO) PostWaypoint(destinationID int64, clearOthers bool, addToBeginning bool) error {
	tok, err := s.ensureAccessToken(context.Background())
	if err != nil {
		return err
	}
	q := url.Values{}
	q.Set("destination_id", strconv.FormatInt(destinationID, 10))
	q.Set("add_to_beginning", strconv.FormatBool(addToBeginning))
	q.Set("clear_other_waypoints", strconv.FormatBool(clearOthers))
	q.Set("datasource", "tranquility")
	endpoint := esiBase + "/v2/ui/autopilot/waypoint?" + q.Encode()

	fmt.Printf("ESI: POST waypoint dest=%d clear=%v addToBeginning=%v\n", destinationID, clearOthers, addToBeginning)
	req, err := http.NewRequest(http.MethodPost, endpoint, nil)
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+tok)
	req.Header.Set("Accept", "application/json")
	req.Header.Set("X-User-Agent", "signalerr/1.0")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusNoContent || resp.StatusCode == http.StatusOK {
		fmt.Println("ESI: waypoint set OK", resp.Status)
		return nil
	}
	b, _ := io.ReadAll(resp.Body)
	fmt.Printf("ESI: waypoint failed %s body=%s\n", resp.Status, string(b))
	return fmt.Errorf("waypoint failed: %s: %s", resp.Status, string(b))
}

// Status reports current login state and character details
type SSOStatus struct {
	LoggedIn      bool
	CharacterID   int64
	CharacterName string
	ExpiresAt     time.Time
}

func (s *ESISSO) Status() SSOStatus {
	s.mu.Lock()
	defer s.mu.Unlock()
	return SSOStatus{
		LoggedIn:      s.accessToken != "",
		CharacterID:   s.characterID,
		CharacterName: s.characterName,
		ExpiresAt:     s.expiresAt,
	}
}

// ResolveSystemIDByName searches ESI for a solar system by name and returns its ID
func (s *ESISSO) ResolveSystemIDByName(ctx context.Context, name string) (int64, error) {
	name = strings.TrimSpace(name)
	if name == "" {
		return 0, errors.New("empty system name")
	}
	// 1) Prefer universe/ids (name->id) for accuracy
	type idsReq struct {
		Names []string `json:"names"`
	}
	body, _ := json.Marshal(idsReq{Names: []string{name}})
	idsURL := esiBase + "/v3/universe/ids/?datasource=tranquility"
	fmt.Printf("ESI: resolve system id via universe/ids for name=%q\n", name)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, idsURL, strings.NewReader(string(body)))
	if err == nil {
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Accept", "application/json")
		resp, errDo := http.DefaultClient.Do(req)
		if errDo == nil {
			defer resp.Body.Close()
			if resp.StatusCode == http.StatusOK {
				var idsResp struct {
					Systems []struct {
						ID   int64  `json:"id"`
						Name string `json:"name"`
					} `json:"systems"`
				}
				if err := json.NewDecoder(resp.Body).Decode(&idsResp); err == nil {
					if len(idsResp.Systems) > 0 {
						fmt.Printf("ESI: universe/ids hit: %s -> %d\n", idsResp.Systems[0].Name, idsResp.Systems[0].ID)
						return idsResp.Systems[0].ID, nil
					}
				}
			}
		}
	}

	// 2) Fallback: strict search
	q := url.Values{}
	q.Set("categories", "solar_system")
	q.Set("search", name)
	q.Set("strict", "true")
	searchURL := esiBase + "/v3/search/?" + q.Encode()
	fmt.Printf("ESI: strict search for %q\n", name)
	req2, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil)
	if err != nil {
		return 0, err
	}
	req2.Header.Set("Accept", "application/json")
	resp2, err := http.DefaultClient.Do(req2)
	if err == nil {
		defer resp2.Body.Close()
		if resp2.StatusCode == http.StatusOK {
			var payload struct {
				SolarSystem []int64 `json:"solar_system"`
			}
			if err := json.NewDecoder(resp2.Body).Decode(&payload); err == nil && len(payload.SolarSystem) > 0 {
				fmt.Printf("ESI: strict search hit: %d\n", payload.SolarSystem[0])
				return payload.SolarSystem[0], nil
			}
		}
	}

	// 3) Fallback: non-strict search then best-effort name match via universe/names
	q.Set("strict", "false")
	searchURL2 := esiBase + "/v3/search/?" + q.Encode()
	fmt.Printf("ESI: non-strict search for %q\n", name)
	req3, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL2, nil)
	if err != nil {
		return 0, err
	}
	req3.Header.Set("Accept", "application/json")
	resp3, err := http.DefaultClient.Do(req3)
	if err != nil {
		return 0, err
	}
	defer resp3.Body.Close()
	if resp3.StatusCode != http.StatusOK {
		b, _ := io.ReadAll(resp3.Body)
		return 0, fmt.Errorf("search failed: %s: %s", resp3.Status, string(b))
	}
	var payload struct {
		SolarSystem []int64 `json:"solar_system"`
	}
	if err := json.NewDecoder(resp3.Body).Decode(&payload); err != nil || len(payload.SolarSystem) == 0 {
		return 0, fmt.Errorf("system not found: %s", name)
	}
	// If one result, return it
	if len(payload.SolarSystem) == 1 {
		fmt.Printf("ESI: non-strict search single hit: %d\n", payload.SolarSystem[0])
		return payload.SolarSystem[0], nil
	}
	// Multiple: resolve names and pick exact case-insensitive match if possible
	ids := payload.SolarSystem
	var idNamesReq = make([]int64, 0, len(ids))
	idNamesReq = append(idNamesReq, ids...)

	namesURL := esiBase + "/v3/universe/names/?datasource=tranquility"
	idsBody, _ := json.Marshal(idNamesReq)
	req4, err := http.NewRequestWithContext(ctx, http.MethodPost, namesURL, strings.NewReader(string(idsBody)))
	if err != nil {
		return 0, err
	}
	req4.Header.Set("Content-Type", "application/json")
	req4.Header.Set("Accept", "application/json")
	resp4, err := http.DefaultClient.Do(req4)
	if err != nil {
		return 0, err
	}
	defer resp4.Body.Close()
	if resp4.StatusCode != http.StatusOK {
		b, _ := io.ReadAll(resp4.Body)
		return 0, fmt.Errorf("names lookup failed: %s: %s", resp4.Status, string(b))
	}
	var namesResp []struct {
		Category string `json:"category"`
		ID       int64  `json:"id"`
		Name     string `json:"name"`
	}
	if err := json.NewDecoder(resp4.Body).Decode(&namesResp); err != nil {
		return 0, err
	}
	lower := strings.ToLower(name)
	for _, n := range namesResp {
		if n.Category == "solar_system" && strings.ToLower(n.Name) == lower {
			fmt.Printf("ESI: names resolved exact: %s -> %d\n", n.Name, n.ID)
			return n.ID, nil
		}
	}
	// Fallback: return first
	fmt.Printf("ESI: names resolved fallback: returning %d for %q\n", namesResp[0].ID, name)
	return namesResp[0].ID, nil
}

// Helpers

type tokenResponse struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int    `json:"expires_in"`
}

func generatePKCE() (verifier string, challenge string, err error) {
	buf := make([]byte, 32)
	if _, err = rand.Read(buf); err != nil {
		return
	}
	v := base64.RawURLEncoding.EncodeToString(buf)
	h := sha256.Sum256([]byte(v))
	c := base64.RawURLEncoding.EncodeToString(h[:])
	return v, c, nil
}

func randString(n int) string {
	buf := make([]byte, n)
	_, _ = rand.Read(buf)
	return base64.RawURLEncoding.EncodeToString(buf)
}

func parseTokenCharacter(jwt string) (name string, id int64) {
	parts := strings.Split(jwt, ".")
	if len(parts) != 3 {
		return "", 0
	}
	payload, err := base64.RawURLEncoding.DecodeString(parts[1])
	if err != nil {
		return "", 0
	}
	var m map[string]any
	if err := json.Unmarshal(payload, &m); err != nil {
		return "", 0
	}
	if v, ok := m["name"].(string); ok {
		name = v
	}
	if v, ok := m["sub"].(string); ok {
		// format EVE:CHARACTER:<id>
		if idx := strings.LastIndexByte(v, ':'); idx > -1 {
			if idv, err := strconv.ParseInt(v[idx+1:], 10, 64); err == nil {
				id = idv
			}
		}
	}
	return
} 