package auth

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
)

const githubCopilotClientID = "Iv1.b507a08c87ecfe98"

// GitHubCopilotDeviceAuthorization is GitHub's OAuth 2 device-code
// response used for Copilot subscription login.
type GitHubCopilotDeviceAuthorization struct {
	DeviceCode      string `json:"device_code"`
	UserCode        string `json:"user_code"`
	VerificationURI string `json:"verification_uri"`
	ExpiresIn       int    `json:"expires_in"`
	Interval        int    `json:"interval"`
}

// RequestGitHubCopilotDeviceAuthorization starts GitHub Copilot's
// device-code login. The resulting GitHub access token is later traded
// for short-lived Copilot inference tokens by the provider client.
func RequestGitHubCopilotDeviceAuthorization(ctx context.Context) (GitHubCopilotDeviceAuthorization, error) {
	form := url.Values{}
	form.Set("client_id", githubCopilotClientID)
	form.Set("scope", "read:user")
	req, err := http.NewRequestWithContext(ctx, "POST", "https://github.com/login/device/code", bytes.NewBufferString(form.Encode()))
	if err != nil {
		return GitHubCopilotDeviceAuthorization{}, err
	}
	req.Header.Set("content-type", "application/x-www-form-urlencoded")
	req.Header.Set("accept", "application/json")
	req.Header.Set("user-agent", "GitHubCopilotChat/0.35.0")

	resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
	if err != nil {
		return GitHubCopilotDeviceAuthorization{}, fmt.Errorf("github copilot device authorization: %w", err)
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return GitHubCopilotDeviceAuthorization{}, fmt.Errorf("github copilot device authorization http %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
	}
	var out GitHubCopilotDeviceAuthorization
	if err := json.Unmarshal(body, &out); err != nil {
		return out, fmt.Errorf("parse github copilot device authorization: %w", err)
	}
	if out.Interval <= 0 {
		out.Interval = 5
	}
	return out, nil
}

// PollGitHubCopilotDeviceToken polls until GitHub's browser/device-code
// login completes and returns the GitHub access token.
func PollGitHubCopilotDeviceToken(ctx context.Context, auth GitHubCopilotDeviceAuthorization) (*OAuthToken, error) {
	interval := time.Duration(auth.Interval) * time.Second
	if interval <= 0 {
		interval = 5 * time.Second
	}
	deadline := time.Now().Add(time.Duration(auth.ExpiresIn) * time.Second)
	for {
		if auth.ExpiresIn > 0 && time.Now().After(deadline) {
			return nil, fmt.Errorf("github copilot device login expired")
		}
		tok, retry, err := pollGitHubCopilotDeviceTokenOnce(ctx, auth.DeviceCode, interval)
		if err != nil {
			return nil, err
		}
		if tok != nil {
			return tok, nil
		}
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		case <-time.After(retry):
		}
	}
}

func pollGitHubCopilotDeviceTokenOnce(ctx context.Context, deviceCode string, interval time.Duration) (*OAuthToken, time.Duration, error) {
	form := url.Values{}
	form.Set("client_id", githubCopilotClientID)
	form.Set("device_code", deviceCode)
	form.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
	req, err := http.NewRequestWithContext(ctx, "POST", "https://github.com/login/oauth/access_token", bytes.NewBufferString(form.Encode()))
	if err != nil {
		return nil, 0, err
	}
	req.Header.Set("content-type", "application/x-www-form-urlencoded")
	req.Header.Set("accept", "application/json")
	req.Header.Set("user-agent", "GitHubCopilotChat/0.35.0")
	resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req)
	if err != nil {
		return nil, 0, fmt.Errorf("github copilot token poll: %w", err)
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	var raw struct {
		AccessToken      string `json:"access_token"`
		TokenType        string `json:"token_type"`
		Scope            string `json:"scope"`
		Error            string `json:"error"`
		ErrorDescription string `json:"error_description"`
	}
	_ = json.Unmarshal(body, &raw)
	if resp.StatusCode == http.StatusOK && raw.AccessToken != "" {
		return &OAuthToken{
			AccessToken: raw.AccessToken,
			TokenType:   raw.TokenType,
			Scope:       raw.Scope,
			ClientID:    githubCopilotClientID,
		}, 0, nil
	}
	if raw.Error == "authorization_pending" || resp.StatusCode == http.StatusBadRequest {
		return nil, interval, nil
	}
	if raw.Error == "slow_down" {
		return nil, interval + 5*time.Second, nil
	}
	if raw.Error != "" {
		return nil, 0, fmt.Errorf("github copilot token poll: %s: %s", raw.Error, raw.ErrorDescription)
	}
	return nil, 0, fmt.Errorf("github copilot token poll http %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
