package authentik

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

type Client struct {
	baseURL    string
	token      string
	httpClient *http.Client
}

func NewClient(baseURL, token string) *Client {
	return &Client{
		baseURL: strings.TrimRight(baseURL, "/"),
		token:   token,
		httpClient: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

type listResponse[T any] struct {
	Results []T `json:"results"`
}

type flowRef struct {
	PK   string `json:"pk"`
	Slug string `json:"slug"`
}

type oauth2Provider struct {
	PK           int    `json:"pk"`
	Name         string `json:"name"`
	ClientID     string `json:"client_id"`
	RedirectURIs string `json:"redirect_uris"`
}

type application struct {
	PK       int    `json:"pk"`
	Slug     string `json:"slug"`
	Name     string `json:"name"`
	Provider int    `json:"provider"`
}

type scopeMapping struct {
	PK        string `json:"pk"`
	ScopeName string `json:"scope_name"`
}

type certKeyPair struct {
	PK   string `json:"pk"`
	Name string `json:"name"`
}

func (c *Client) Ping(ctx context.Context) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/api/v3/root/config/", nil)
	if err != nil {
		return err
	}
	c.authorize(req)
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("authentik ping: %d", resp.StatusCode)
	}
	return nil
}

func (c *Client) authorize(req *http.Request) {
	if c.token != "" {
		req.Header.Set("Authorization", "Bearer "+c.token)
	}
	req.Header.Set("Accept", "application/json")
}

func (c *Client) FindApplicationBySlug(ctx context.Context, slug string) (*application, bool, error) {
	q := url.Values{}
	q.Set("slug", slug)
	var out listResponse[application]
	if err := c.getJSON(ctx, "/api/v3/core/applications/?"+q.Encode(), &out); err != nil {
		return nil, false, err
	}
	if len(out.Results) == 0 {
		return nil, false, nil
	}
	return &out.Results[0], true, nil
}

func (c *Client) FindOAuth2ProviderByName(ctx context.Context, name string) (*oauth2Provider, bool, error) {
	q := url.Values{}
	q.Set("name", name)
	var out listResponse[oauth2Provider]
	if err := c.getJSON(ctx, "/api/v3/providers/oauth2/?"+q.Encode(), &out); err != nil {
		return nil, false, err
	}
	if len(out.Results) == 0 {
		return nil, false, nil
	}
	return &out.Results[0], true, nil
}

func (c *Client) FindFlowBySlug(ctx context.Context, slug string) (string, error) {
	q := url.Values{}
	q.Set("slug", slug)
	var out listResponse[flowRef]
	if err := c.getJSON(ctx, "/api/v3/flows/instances/?"+q.Encode(), &out); err != nil {
		return "", err
	}
	if len(out.Results) == 0 {
		return "", fmt.Errorf("flow not found: %s", slug)
	}
	return out.Results[0].PK, nil
}

func (c *Client) FindScopeMappingPK(ctx context.Context, scopeName string) (string, error) {
	q := url.Values{}
	q.Set("scope_name", scopeName)
	var out listResponse[scopeMapping]
	if err := c.getJSON(ctx, "/api/v3/propertymappings/provider/oauth2/?"+q.Encode(), &out); err != nil {
		return "", err
	}
	if len(out.Results) == 0 {
		return "", fmt.Errorf("scope mapping not found: %s", scopeName)
	}
	return out.Results[0].PK, nil
}

func (c *Client) FindSigningKeyPK(ctx context.Context) (string, error) {
	var out listResponse[certKeyPair]
	if err := c.getJSON(ctx, "/api/v3/crypto/certificatekeypairs/", &out); err != nil {
		return "", err
	}
	for _, k := range out.Results {
		if strings.Contains(k.Name, "authentik") && strings.Contains(k.Name, "Certificate") {
			return k.PK, nil
		}
	}
	if len(out.Results) > 0 {
		return out.Results[0].PK, nil
	}
	return "", fmt.Errorf("no signing key found")
}

type CreateOAuth2ProviderRequest struct {
	Name                string
	ClientID            string
	ClientSecret        string
	RedirectURIs        []string
	AuthorizationFlowPK string
	InvalidationFlowPK  string
	SigningKeyPK        string
	PropertyMappingPKs  []string
	OfflineAccess       bool
}

func (c *Client) CreateOAuth2Provider(ctx context.Context, req CreateOAuth2ProviderRequest) (int, error) {
	mappings := req.PropertyMappingPKs
	if req.OfflineAccess {
		if pk, err := c.FindScopeMappingPK(ctx, "offline_access"); err == nil {
			mappings = append(mappings, pk)
		}
	}
	body := map[string]any{
		"name":                 req.Name,
		"authorization_flow":   req.AuthorizationFlowPK,
		"invalidation_flow":    req.InvalidationFlowPK,
		"property_mappings":      mappings,
		"client_type":          "confidential",
		"client_id":              req.ClientID,
		"client_secret":          req.ClientSecret,
		"redirect_uris":          joinRedirectURIs(req.RedirectURIs),
		"signing_key":            req.SigningKeyPK,
		"access_token_validity":  "hours=1",
		"refresh_token_validity": "days=365",
	}
	var created oauth2Provider
	if err := c.postJSON(ctx, "/api/v3/providers/oauth2/", body, &created); err != nil {
		return 0, err
	}
	return created.PK, nil
}

func (c *Client) UpdateOAuth2ProviderRedirects(ctx context.Context, providerPK int, redirectURIs []string) error {
	body := map[string]any{
		"redirect_uris": joinRedirectURIs(redirectURIs),
	}
	return c.patchJSON(ctx, fmt.Sprintf("/api/v3/providers/oauth2/%d/", providerPK), body)
}

type CreateApplicationRequest struct {
	Name      string
	Slug      string
	Group     string
	LaunchURL string
	Provider  int
}

func (c *Client) CreateApplication(ctx context.Context, req CreateApplicationRequest) (int, error) {
	body := map[string]any{
		"name":              req.Name,
		"slug":              req.Slug,
		"group":             req.Group,
		"provider":          req.Provider,
		"meta_launch_url":   req.LaunchURL,
		"policy_engine_mode": "any",
	}
	var created application
	if err := c.postJSON(ctx, "/api/v3/core/applications/", body, &created); err != nil {
		return 0, err
	}
	return created.PK, nil
}

func joinRedirectURIs(uris []string) string {
	seen := make(map[string]struct{}, len(uris))
	var lines []string
	for _, u := range uris {
		u = strings.TrimSpace(u)
		if u == "" {
			continue
		}
		if _, ok := seen[u]; ok {
			continue
		}
		seen[u] = struct{}{}
		lines = append(lines, u)
	}
	return strings.Join(lines, "\n")
}

func (c *Client) getJSON(ctx context.Context, path string, dest any) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
	if err != nil {
		return err
	}
	c.authorize(req)
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return decodeResponse(resp, dest)
}

func (c *Client) postJSON(ctx context.Context, path string, body any, dest any) error {
	return c.doJSON(ctx, http.MethodPost, path, body, dest)
}

func (c *Client) patchJSON(ctx context.Context, path string, body any) error {
	return c.doJSON(ctx, http.MethodPatch, path, body, nil)
}

func (c *Client) doJSON(ctx context.Context, method, path string, body any, dest any) error {
	var reader io.Reader
	if body != nil {
		raw, err := json.Marshal(body)
		if err != nil {
			return err
		}
		reader = bytes.NewReader(raw)
	}
	req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, reader)
	if err != nil {
		return err
	}
	c.authorize(req)
	req.Header.Set("Content-Type", "application/json")
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return decodeResponse(resp, dest)
}

func decodeResponse(resp *http.Response, dest any) error {
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
		return fmt.Errorf("authentik api %s: %d %s", resp.Request.URL.Path, resp.StatusCode, strings.TrimSpace(string(b)))
	}
	if dest == nil {
		return nil
	}
	return json.NewDecoder(resp.Body).Decode(dest)
}
