package service

import (
	"baron-sso-backend/internal/domain"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"
)

var ErrHydraNotFound = errors.New("hydra admin: resource not found")

// HydraAdminService는 Hydra Admin API 호출을 래핑합니다.
type HydraAdminService struct {
	AdminURL   string
	PublicURL  string
	HTTPClient *http.Client
}

func NewHydraAdminService() *HydraAdminService {
	return &HydraAdminService{
		AdminURL:  getenv("HYDRA_ADMIN_URL", "http://hydra:4445"),
		PublicURL: getenv("HYDRA_PUBLIC_URL", "http://hydra:4444"),
	}
}

func (s *HydraAdminService) ListClients(ctx context.Context, limit, offset int) ([]domain.HydraClient, error) {
	endpoint, err := s.buildURL("/clients", map[string]int{
		"limit":  limit,
		"offset": offset,
	})
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}

	resp, err := s.httpClient().Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusNotFound {
		return nil, ErrHydraNotFound
	}
	if resp.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return nil, fmt.Errorf("hydra admin: list clients failed status=%d body=%s", resp.StatusCode, string(body))
	}

	var clients []domain.HydraClient
	if err := json.NewDecoder(resp.Body).Decode(&clients); err != nil {
		return nil, fmt.Errorf("hydra admin: decode clients failed: %w", err)
	}
	return clients, nil
}

func (s *HydraAdminService) GetClient(ctx context.Context, clientID string) (*domain.HydraClient, error) {
	endpoint := fmt.Sprintf("%s/clients/%s", strings.TrimRight(s.AdminURL, "/"), url.PathEscape(clientID))
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}

	resp, err := s.httpClient().Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusNotFound {
		return nil, ErrHydraNotFound
	}
	if resp.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return nil, fmt.Errorf("hydra admin: get client failed status=%d body=%s", resp.StatusCode, string(body))
	}

	var client domain.HydraClient
	if err := json.NewDecoder(resp.Body).Decode(&client); err != nil {
		return nil, fmt.Errorf("hydra admin: decode client failed: %w", err)
	}
	return &client, nil
}

func (s *HydraAdminService) PatchClientStatus(ctx context.Context, clientID, status string) (*domain.HydraClient, error) {
	// JSON Patch format
	payload := []map[string]interface{}{
		{
			"op":    "replace",
			"path":  "/metadata/status",
			"value": status,
		},
	}
	body, _ := json.Marshal(payload)
	endpoint := fmt.Sprintf("%s/clients/%s", strings.TrimRight(s.AdminURL, "/"), url.PathEscape(clientID))

	req, err := http.NewRequestWithContext(ctx, http.MethodPatch, endpoint, bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json-patch+json")

	resp, err := s.httpClient().Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusNotFound {
		return nil, ErrHydraNotFound
	}
	if resp.StatusCode >= 300 {
		respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return nil, fmt.Errorf("hydra admin: patch client failed status=%d body=%s", resp.StatusCode, string(respBody))
	}

	var updated domain.HydraClient
	if err := json.NewDecoder(resp.Body).Decode(&updated); err != nil {
		return nil, fmt.Errorf("hydra admin: decode patched client failed: %w", err)
	}
	return &updated, nil
}

func (s *HydraAdminService) CreateClient(ctx context.Context, client domain.HydraClient) (*domain.HydraClient, error) {
	body, _ := json.Marshal(client)
	endpoint := fmt.Sprintf("%s/clients", strings.TrimRight(s.AdminURL, "/"))

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := s.httpClient().Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 300 {
		respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return nil, fmt.Errorf("hydra admin: create client failed status=%d body=%s", resp.StatusCode, string(respBody))
	}

	var created domain.HydraClient
	if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
		return nil, fmt.Errorf("hydra admin: decode created client failed: %w", err)
	}
	return &created, nil
}

func (s *HydraAdminService) UpdateClient(ctx context.Context, clientID string, client domain.HydraClient) (*domain.HydraClient, error) {
	client.ClientID = clientID
	body, _ := json.Marshal(client)
	endpoint := fmt.Sprintf("%s/clients/%s", strings.TrimRight(s.AdminURL, "/"), url.PathEscape(clientID))

	req, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := s.httpClient().Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusNotFound {
		return nil, ErrHydraNotFound
	}
	if resp.StatusCode >= 300 {
		respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return nil, fmt.Errorf("hydra admin: update client failed status=%d body=%s", resp.StatusCode, string(respBody))
	}

	var updated domain.HydraClient
	if err := json.NewDecoder(resp.Body).Decode(&updated); err != nil {
		return nil, fmt.Errorf("hydra admin: decode updated client failed: %w", err)
	}
	return &updated, nil
}

func (s *HydraAdminService) DeleteClient(ctx context.Context, clientID string) error {
	endpoint := fmt.Sprintf("%s/clients/%s", strings.TrimRight(s.AdminURL, "/"), url.PathEscape(clientID))
	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)
	if err != nil {
		return err
	}

	resp, err := s.httpClient().Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusNotFound {
		return ErrHydraNotFound
	}
	if resp.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return fmt.Errorf("hydra admin: delete client failed status=%d body=%s", resp.StatusCode, string(body))
	}
	return nil
}

func (s *HydraAdminService) ListConsentSessions(ctx context.Context, subject, clientID string) ([]domain.HydraConsentSession, error) {
	params := map[string]string{
		"subject": subject,
	}
	if clientID != "" {
		params["client"] = clientID
	}
	endpoint, err := s.buildURLWithParams("/oauth2/auth/sessions/consent", params)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}

	resp, err := s.httpClient().Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusNoContent {
		return []domain.HydraConsentSession{}, nil
	}
	body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
	if resp.StatusCode >= 300 {
		return nil, fmt.Errorf("hydra admin: list consent sessions failed status=%d body=%s", resp.StatusCode, string(body))
	}
	if len(body) == 0 {
		return []domain.HydraConsentSession{}, nil
	}

	var sessions []domain.HydraConsentSession
	if err := json.Unmarshal(body, &sessions); err != nil {
		return nil, fmt.Errorf("hydra admin: decode consent sessions failed: %w body=%s", err, string(body))
	}
	return sessions, nil
}

func (s *HydraAdminService) RevokeConsentSessions(ctx context.Context, subject, clientID string) error {
	params := map[string]string{
		"subject": subject,
	}
	if clientID != "" {
		params["client"] = clientID
	} else {
		params["all"] = "true"
	}
	endpoint, err := s.buildURLWithParams("/oauth2/auth/sessions/consent", params)
	if err != nil {
		return err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)
	if err != nil {
		return err
	}

	resp, err := s.httpClient().Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return fmt.Errorf("hydra admin: revoke consent failed status=%d body=%s", resp.StatusCode, string(body))
	}
	return nil
}

func (s *HydraAdminService) httpClient() *http.Client {
	if s.HTTPClient != nil {
		return s.HTTPClient
	}
	return &http.Client{
		Timeout: 10 * time.Second,
		Transport: &http.Transport{
			DialContext: (&net.Dialer{
				Timeout:   5 * time.Second,
				KeepAlive: 30 * time.Second,
			}).DialContext,
			TLSHandshakeTimeout: 5 * time.Second,
		},
	}
}

func (s *HydraAdminService) buildURL(path string, ints map[string]int) (string, error) {
	base := strings.TrimRight(s.AdminURL, "/")
	u, err := url.Parse(base + path)
	if err != nil {
		return "", err
	}
	q := u.Query()
	for key, value := range ints {
		if value > 0 {
			q.Set(key, strconv.Itoa(value))
		}
	}
	u.RawQuery = q.Encode()
	return u.String(), nil
}

func (s *HydraAdminService) buildURLWithParams(path string, params map[string]string) (string, error) {
	base := strings.TrimRight(s.AdminURL, "/")
	u, err := url.Parse(base + path)
	if err != nil {
		return "", err
	}
	q := u.Query()
	for key, value := range params {
		if value != "" {
			q.Set(key, value)
		}
	}
	u.RawQuery = q.Encode()
	return u.String(), nil
}

type AcceptLoginRequestResponse struct {
	RedirectTo string `json:"redirectTo"`
}

type AcceptConsentRequestResponse struct {
	RedirectTo string `json:"redirectTo"`
}

type RejectConsentRequestResponse struct {
	RedirectTo string `json:"redirectTo"`
}

type RejectLoginRequestResponse struct {
	RedirectTo string `json:"redirectTo"`
}

func (s *HydraAdminService) GetConsentRequest(ctx context.Context, challenge string) (*domain.HydraConsentRequest, error) {
	params := map[string]string{
		"consent_challenge": challenge,
	}
	endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/consent", params)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, fmt.Errorf("hydra admin: create request for get consent failed: %w", err)
	}

	resp, err := s.httpClient().Do(req)
	if err != nil {
		return nil, fmt.Errorf("hydra admin: get consent request failed: %w", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("hydra admin: get consent failed status=%d body=%s", resp.StatusCode, string(body))
	}

	var consentReq domain.HydraConsentRequest
	if err := json.Unmarshal(body, &consentReq); err != nil {
		return nil, fmt.Errorf("hydra admin: decode get consent response failed: %w", err)
	}

	return &consentReq, nil
}

func (s *HydraAdminService) RejectConsentRequest(ctx context.Context, challenge string) (*RejectConsentRequestResponse, error) {
	params := map[string]string{
		"consent_challenge": challenge,
	}
	endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/consent/reject", params)
	if err != nil {
		return nil, err
	}

	payload := map[string]interface{}{
		"error":             "access_denied",
		"error_description": "The user decided to reject the consent request.",
	}
	body, _ := json.Marshal(payload)

	req, err := http.NewRequestWithContext(ctx, "PUT", endpoint, bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("hydra admin: create request for reject consent failed: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := s.httpClient().Do(req)
	if err != nil {
		return nil, fmt.Errorf("hydra admin: reject consent request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("hydra admin: reject consent failed status=%d body=%s", resp.StatusCode, string(respBody))
	}

	var hydraResp struct {
		RedirectTo string `json:"redirect_to"`
	}
	if err := json.Unmarshal(respBody, &hydraResp); err != nil {
		return nil, fmt.Errorf("hydra admin: decode reject consent response failed: %w", err)
	}

	return &RejectConsentRequestResponse{RedirectTo: hydraResp.RedirectTo}, nil
}

func (s *HydraAdminService) RejectLoginRequest(ctx context.Context, challenge, error, errorDescription string) (*RejectLoginRequestResponse, error) {
	params := map[string]string{
		"login_challenge": challenge,
	}
	endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/login/reject", params)
	if err != nil {
		return nil, err
	}

	payload := map[string]interface{}{
		"error":             error,
		"error_description": errorDescription,
	}
	body, _ := json.Marshal(payload)

	req, err := http.NewRequestWithContext(ctx, "PUT", endpoint, bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("hydra admin: create request for reject login failed: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := s.httpClient().Do(req)
	if err != nil {
		return nil, fmt.Errorf("hydra admin: reject login request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("hydra admin: reject login failed status=%d body=%s", resp.StatusCode, string(respBody))
	}

	var hydraResp struct {
		RedirectTo string `json:"redirect_to"`
	}
	if err := json.Unmarshal(respBody, &hydraResp); err != nil {
		return nil, fmt.Errorf("hydra admin: decode reject login response failed: %w", err)
	}

	return &RejectLoginRequestResponse{RedirectTo: hydraResp.RedirectTo}, nil
}

func (s *HydraAdminService) GetLoginRequest(ctx context.Context, challenge string) (*domain.HydraLoginRequest, error) {
	params := map[string]string{
		"login_challenge": challenge,
	}
	endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/login", params)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, fmt.Errorf("hydra admin: create request for get login failed: %w", err)
	}

	resp, err := s.httpClient().Do(req)
	if err != nil {
		return nil, fmt.Errorf("hydra admin: get login request failed: %w", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("hydra admin: get login failed status=%d body=%s", resp.StatusCode, string(body))
	}

	var loginReq domain.HydraLoginRequest
	if err := json.Unmarshal(body, &loginReq); err != nil {
		return nil, fmt.Errorf("hydra admin: decode get login response failed: %w", err)
	}

	return &loginReq, nil
}

func (s *HydraAdminService) AcceptConsentRequest(ctx context.Context, challenge string, grantInfo *domain.HydraConsentRequest, sessionClaims map[string]any) (*AcceptConsentRequestResponse, error) {
	params := map[string]string{
		"consent_challenge": challenge,
	}
	endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/consent/accept", params)
	if err != nil {
		return nil, err
	}

	payload := map[string]interface{}{
		"grant_scope":    grantInfo.RequestedScope,
		"grant_audience": grantInfo.RequestedAudience,
		"remember":       true,
		"remember_for":   2592000,
	}
	if len(sessionClaims) > 0 {
		payload["session"] = map[string]any{
			"id_token":     sessionClaims,
			"access_token": sessionClaims,
		}
	}
	body, _ := json.Marshal(payload)

	req, err := http.NewRequestWithContext(ctx, "PUT", endpoint, bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("hydra admin: create request for accept consent failed: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := s.httpClient().Do(req)
	if err != nil {
		return nil, fmt.Errorf("hydra admin: accept consent request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("hydra admin: accept consent failed status=%d body=%s", resp.StatusCode, string(respBody))
	}

	// Hydra 응답(redirect_to)을 읽어서 우리 응답(redirectTo)으로 변환
	var hydraResp struct {
		RedirectTo string `json:"redirect_to"`
	}
	if err := json.Unmarshal(respBody, &hydraResp); err != nil {
		return nil, fmt.Errorf("hydra admin: decode accept consent response failed: %w", err)
	}

	return &AcceptConsentRequestResponse{RedirectTo: hydraResp.RedirectTo}, nil
}

func (s *HydraAdminService) AcceptLoginRequest(ctx context.Context, challenge string, subject string) (*AcceptLoginRequestResponse, error) {
	params := map[string]string{
		"login_challenge": challenge,
	}
	endpoint, err := s.buildURLWithParams("/oauth2/auth/requests/login/accept", params)
	if err != nil {
		return nil, err
	}

	payload := map[string]interface{}{
		"subject":      subject,
		"remember":     true,
		"remember_for": 2592000,
	}
	body, _ := json.Marshal(payload)

	req, err := http.NewRequestWithContext(ctx, "PUT", endpoint, bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("hydra admin: create request for accept login failed: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := s.httpClient().Do(req)
	if err != nil {
		return nil, fmt.Errorf("hydra admin: accept login request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("hydra admin: accept login failed status=%d body=%s", resp.StatusCode, string(respBody))
	}

	// Hydra 응답(redirect_to)을 읽어서 우리 응답(redirectTo)으로 변환
	var hydraResp struct {
		RedirectTo string `json:"redirect_to"`
	}
	if err := json.Unmarshal(respBody, &hydraResp); err != nil {
		return nil, fmt.Errorf("hydra admin: decode accept login response failed: %w", err)
	}

	return &AcceptLoginRequestResponse{RedirectTo: hydraResp.RedirectTo}, nil
}

type HydraIntrospectionResponse struct {
	Active    bool                   `json:"active"`
	Subject   string                 `json:"sub"`
	ClientID  string                 `json:"client_id"`
	Scope     string                 `json:"scope"`
	ExpiresAt int64                  `json:"exp"`
	IssuedAt  int64                  `json:"iat"`
	Ext       map[string]interface{} `json:"ext"`
}

func (s *HydraAdminService) IntrospectToken(ctx context.Context, token string) (*HydraIntrospectionResponse, error) {
	endpoint := fmt.Sprintf("%s/oauth2/introspect", strings.TrimRight(s.AdminURL, "/"))
	form := url.Values{}
	form.Set("token", token)

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

	resp, err := s.httpClient().Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return nil, fmt.Errorf("hydra admin: introspection failed status=%d body=%s", resp.StatusCode, string(body))
	}

	var res HydraIntrospectionResponse
	if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
		return nil, err
	}
	return &res, nil
}
