package adapter

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/url"
	"strings"
	"sync"
)

// sessionRequest is usher's POST /api/sessions body.
//
// InitialMessage is why there is no second call: usher delivers it to the agent
// as the opening turn, so creating the session and asking the question are one
// request. AutoApprove is the other half of running unattended — without it a
// permission prompt waits for a human who is not there.
type sessionRequest struct {
	Agent          string `json:"agent"`
	Cwd            string `json:"cwd"`
	InitialMessage string `json:"initial_message"`
	Backend        string `json:"backend"`
	Model          string `json:"model"`
	AutoApprove    *bool  `json:"auto_approve"`
}

// usherClient speaks usher's browser API.
//
// usher authenticates people, not machines: a password, and a cookie MAC'd over
// its hash. There is no bearer token to hold, so this logs in the way the web UI
// does and keeps the cookie, refreshing it when a request comes back bounced.
// When usher runs with no password configured, /login is not even routed and
// none of this engages — both arrangements work with nothing to configure here.
type usherClient struct {
	baseURL  string
	password string
	http     *http.Client
	logger   *slog.Logger

	mu     sync.RWMutex
	cookie string
}

func newUsherClient(baseURL, password string, client *http.Client, logger *slog.Logger) *usherClient {
	return &usherClient{baseURL: baseURL, password: password, http: client, logger: logger}
}

func (c *usherClient) startSession(ctx context.Context, req sessionRequest) (string, error) {
	body, err := json.Marshal(req)
	if err != nil {
		return "", err
	}
	resp, err := c.do(ctx, http.MethodPost, "/api/sessions", "application/json", body)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	payload, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
	if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("usher create session: %d %s",
			resp.StatusCode, strings.TrimSpace(string(payload)))
	}
	var created struct {
		ID string `json:"id"`
	}
	if err := json.Unmarshal(payload, &created); err != nil {
		return "", fmt.Errorf("usher create session: unreadable response: %w", err)
	}
	if created.ID == "" {
		return "", fmt.Errorf("usher create session returned no id")
	}
	return created.ID, nil
}

// do sends the request, and retries it once through a fresh login if the answer
// says the cookie is missing or stale.
func (c *usherClient) do(ctx context.Context, method, path, contentType string, body []byte) (*http.Response, error) {
	resp, err := c.send(ctx, method, path, contentType, body)
	if err != nil {
		return nil, err
	}
	if !bounced(resp) {
		return resp, nil
	}

	resp.Body.Close()
	if c.password == "" {
		// usher is asking for a login this deployment cannot perform. Retrying
		// cannot fix it, so say what is actually wrong.
		return nil, fmt.Errorf("usher requires a password but none is configured")
	}
	if err := c.login(ctx); err != nil {
		return nil, err
	}
	return c.send(ctx, method, path, contentType, body)
}

func (c *usherClient) send(ctx context.Context, method, path, contentType string, body []byte) (*http.Response, error) {
	req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	if contentType != "" {
		req.Header.Set("Content-Type", contentType)
	}
	c.mu.RLock()
	cookie := c.cookie
	c.mu.RUnlock()
	if cookie != "" {
		req.Header.Set("Cookie", cookie)
	}
	return c.http.Do(req)
}

func (c *usherClient) login(ctx context.Context) error {
	form := url.Values{"password": {c.password}}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		c.baseURL+"/login", strings.NewReader(form.Encode()))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := c.http.Do(req)
	if err != nil {
		return fmt.Errorf("usher login: %w", err)
	}
	defer resp.Body.Close()

	for _, ck := range resp.Cookies() {
		if ck.Name == "usher_auth" && ck.Value != "" {
			c.mu.Lock()
			c.cookie = ck.Name + "=" + ck.Value
			c.mu.Unlock()
			c.logger.Info("usher login ok")
			return nil
		}
	}
	return fmt.Errorf("usher login: no session cookie in response (%d)", resp.StatusCode)
}

// bounced reports the two shapes "you are not logged in" arrives in: a 401, or
// a redirect at the login page for a request that expected neither.
func bounced(resp *http.Response) bool {
	if resp.StatusCode == http.StatusUnauthorized {
		return true
	}
	if resp.StatusCode >= 300 && resp.StatusCode < 400 {
		return strings.Contains(resp.Header.Get("Location"), "/login")
	}
	return false
}
