package service

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

	"golang.org/x/crypto/bcrypt"
)

type KratosIdentity struct {
	ID             string                 `json:"id"`
	SchemaID       string                 `json:"schema_id,omitempty"`
	Traits         map[string]interface{} `json:"traits"`
	State          string                 `json:"state,omitempty"`
	MetadataAdmin  interface{}            `json:"metadata_admin,omitempty"`
	MetadataPublic interface{}            `json:"metadata_public,omitempty"`
	ExternalID     string                 `json:"external_id,omitempty"`
	CreatedAt      time.Time              `json:"created_at,omitempty"`
	UpdatedAt      time.Time              `json:"updated_at,omitempty"`
}

type KratosSessionDevice struct {
	UserAgent string `json:"user_agent,omitempty"`
	IPAddress string `json:"ip_address,omitempty"`
}

type KratosSession struct {
	ID              string                `json:"id"`
	Active          bool                  `json:"active"`
	AuthenticatedAt time.Time             `json:"authenticated_at,omitempty"`
	ExpiresAt       time.Time             `json:"expires_at,omitempty"`
	IssuedAt        time.Time             `json:"issued_at,omitempty"`
	Identity        *KratosIdentity       `json:"identity,omitempty"`
	Devices         []KratosSessionDevice `json:"devices,omitempty"`
}

type KratosAdminService interface {
	ListIdentities(ctx context.Context) ([]KratosIdentity, error)
	FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error)
	GetIdentity(ctx context.Context, identityID string) (*KratosIdentity, error)
	UpdateIdentity(ctx context.Context, identityID string, traits map[string]interface{}, state string) (*KratosIdentity, error)
	UpdateIdentityPassword(ctx context.Context, identityID, newPassword string) error
	DeleteIdentity(ctx context.Context, identityID string) error
	CreateUser(ctx context.Context, user *domain.BrokerUser, password string) (string, error)
	ListIdentitySessions(ctx context.Context, identityID string) ([]KratosSession, error)
	GetSession(ctx context.Context, sessionID string) (*KratosSession, error)
	DeleteSession(ctx context.Context, sessionID string) error
}

type kratosAdminService struct {
	AdminURL   string
	HTTPClient *http.Client
}

func NewKratosAdminService() KratosAdminService {
	return &kratosAdminService{
		AdminURL: getenvKratos("KRATOS_ADMIN_URL", "http://kratos:4434"),
	}
}

func (s *kratosAdminService) ListIdentities(ctx context.Context) ([]KratosIdentity, error) {
	endpoint := strings.TrimRight(s.AdminURL, "/") + "/admin/identities"
	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 >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return nil, fmt.Errorf("kratos admin list identities failed status=%d body=%s", resp.StatusCode, string(body))
	}

	var identities []KratosIdentity
	if err := json.NewDecoder(resp.Body).Decode(&identities); err != nil {
		return nil, err
	}
	return identities, nil
}

func (s *kratosAdminService) FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error) {
	identifier = strings.TrimSpace(identifier)
	if identifier == "" {
		return "", nil
	}

	endpoint := strings.TrimRight(s.AdminURL, "/") + "/admin/identities"
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return "", err
	}

	query := req.URL.Query()
	query.Set("credentials_identifier", identifier)
	req.URL.RawQuery = query.Encode()

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

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

	var identities []struct {
		ID string `json:"id"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&identities); err != nil {
		return "", err
	}
	if len(identities) == 0 {
		return "", nil
	}
	return identities[0].ID, nil
}

func (s *kratosAdminService) GetIdentity(ctx context.Context, identityID string) (*KratosIdentity, error) {
	endpoint := fmt.Sprintf("%s/admin/identities/%s", strings.TrimRight(s.AdminURL, "/"), identityID)
	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, nil
	}
	if resp.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return nil, fmt.Errorf("kratos admin get identity failed status=%d body=%s", resp.StatusCode, string(body))
	}

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

func (s *kratosAdminService) UpdateIdentity(ctx context.Context, identityID string, traits map[string]interface{}, state string) (*KratosIdentity, error) {
	payload := map[string]interface{}{
		"schema_id": "default",
		"traits":    traits,
	}
	if strings.TrimSpace(state) != "" {
		payload["state"] = strings.TrimSpace(state)
	}

	body, _ := json.Marshal(payload)
	endpoint := fmt.Sprintf("%s/admin/identities/%s", strings.TrimRight(s.AdminURL, "/"), identityID)
	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 >= 300 {
		respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return nil, fmt.Errorf("kratos admin update identity failed status=%d body=%s", resp.StatusCode, string(respBody))
	}

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

func (s *kratosAdminService) UpdateIdentityPassword(ctx context.Context, identityID, newPassword string) error {
	identity, err := s.GetIdentity(ctx, identityID)
	if err != nil {
		return err
	}
	if identity == nil {
		return fmt.Errorf("kratos admin identity not found: %s", identityID)
	}

	hashedPassword, err := hashPasswordForKratosAdmin(newPassword)
	if err != nil {
		return err
	}

	payload := map[string]interface{}{
		"schema_id": identity.SchemaID,
		"traits":    identity.Traits,
		"state":     identity.State,
		"credentials": map[string]interface{}{
			"password": map[string]interface{}{
				"config": map[string]string{
					"hashed_password": hashedPassword,
				},
			},
		},
	}
	if payload["schema_id"] == "" {
		payload["schema_id"] = "default"
	}
	if payload["state"] == "" {
		payload["state"] = "active"
	}
	if identity.MetadataAdmin != nil {
		payload["metadata_admin"] = identity.MetadataAdmin
	}
	if identity.MetadataPublic != nil {
		payload["metadata_public"] = identity.MetadataPublic
	}
	if identity.ExternalID != "" {
		payload["external_id"] = identity.ExternalID
	}

	body, _ := json.Marshal(payload)
	endpoint := fmt.Sprintf("%s/admin/identities/%s", strings.TrimRight(s.AdminURL, "/"), identityID)
	req, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, bytes.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := s.httpClient().Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return fmt.Errorf("kratos admin update password failed status=%d body=%s", resp.StatusCode, string(respBody))
	}
	return nil
}

func (s *kratosAdminService) CreateUser(ctx context.Context, user *domain.BrokerUser, password string) (string, error) {
	if user == nil {
		return "", fmt.Errorf("kratos admin: user payload is nil")
	}

	traits := map[string]interface{}{
		"email": user.Email,
		"name":  user.Name,
	}
	if user.PhoneNumber != "" {
		traits["phone_number"] = user.PhoneNumber
	}
	for k, v := range user.Attributes {
		if k == "id" || k == "email" {
			continue
		}
		traits[k] = v
	}

	payload := map[string]interface{}{
		"schema_id": "default",
		"traits":    traits,
		"credentials": map[string]interface{}{
			"password": map[string]interface{}{
				"config": map[string]string{
					"password": password,
				},
			},
		},
		"state": "active",
	}

	body, _ := json.Marshal(payload)
	endpoint := strings.TrimRight(s.AdminURL, "/") + "/admin/identities"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
	if err != nil {
		return "", err
	}
	req.Header.Set("Content-Type", "application/json")

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

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

	var created struct {
		ID string `json:"id"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
		return "", err
	}

	return created.ID, nil
}

func hashPasswordForKratosAdmin(password string) (string, error) {
	hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return "", err
	}
	return string(hashed), nil
}

func (s *kratosAdminService) DeleteIdentity(ctx context.Context, identityID string) error {
	endpoint := fmt.Sprintf("%s/admin/identities/%s", strings.TrimRight(s.AdminURL, "/"), identityID)
	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 {
		respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return fmt.Errorf("kratos admin delete identity failed status=%d body=%s", resp.StatusCode, string(respBody))
	}
	return nil
}

func (s *kratosAdminService) 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 getenvKratos(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

func (s *kratosAdminService) ListIdentitySessions(ctx context.Context, identityID string) ([]KratosSession, error) {
	url := fmt.Sprintf("%s/admin/identities/%s/sessions", s.AdminURL, identityID)
	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		return nil, err
	}

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

	if resp.StatusCode == http.StatusNotFound {
		return []KratosSession{}, nil
	}
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
	}

	var sessions []KratosSession
	if err := json.NewDecoder(resp.Body).Decode(&sessions); err != nil {
		return nil, err
	}
	return sessions, nil
}

func (s *kratosAdminService) GetSession(ctx context.Context, sessionID string) (*KratosSession, error) {
	url := fmt.Sprintf("%s/admin/sessions/%s", s.AdminURL, sessionID)
	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		return nil, err
	}

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

	if resp.StatusCode == http.StatusNotFound {
		return nil, nil
	}
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
	}

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

func (s *kratosAdminService) DeleteSession(ctx context.Context, sessionID string) error {
	url := fmt.Sprintf("%s/admin/sessions/%s", s.AdminURL, sessionID)
	req, err := http.NewRequestWithContext(ctx, "DELETE", url, nil)
	if err != nil {
		return err
	}

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

	if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
		return fmt.Errorf("unexpected status: %d", resp.StatusCode)
	}
	return nil
}
