package service

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

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

// OryProvider는 Kratos/Hydra를 기반으로 하는 IDP 어댑터의 최소 스켈레톤입니다.
// 지금은 스키마 메타데이터만 반환하며, 나머지 동작은 후속 작업에서 구현합니다.
type OryProvider struct {
	KratosAdminURL  string
	KratosPublicURL string
	HydraAdminURL   string
	HTTPClient      *http.Client
}

func NewOryProvider() *OryProvider {
	return &OryProvider{
		KratosAdminURL:  getenv("KRATOS_ADMIN_URL", "http://kratos:4434"),
		KratosPublicURL: getenv("KRATOS_PUBLIC_URL", "http://kratos:4433"),
		HydraAdminURL:   getenv("HYDRA_ADMIN_URL", "http://hydra:4445"),
	}
}

func (o *OryProvider) Name() string {
	return "Ory (Kratos/Hydra)"
}

// GetMetadata는 BrokerUser가 요구하는 필드를 Kratos traits에 매핑 가능하다는 가정으로 반환합니다.
func (o *OryProvider) GetMetadata() (*domain.IDPMetadata, error) {
	return &domain.IDPMetadata{
		SupportedFields: []string{
			"id", "custom_login_ids", "login_id", "email", "name", "phone_number",
			"grade", "department", "affiliationType", "tenant_id",
		},
	}, nil
}

// CreateUser는 Kratos Admin API를 통해 identity를 생성합니다.
func (o *OryProvider) CreateUser(user *domain.BrokerUser, password string) (string, error) {
	if user == nil {
		return "", fmt.Errorf("ory provider: user payload is nil")
	}
	if user.Email == "" || password == "" {
		return "", fmt.Errorf("ory provider: email and password are required")
	}

	// 중복 확인
	existingID, err := o.findIdentityID(user.Email)
	if err != nil {
		return "", fmt.Errorf("ory provider: search identity failed: %w", err)
	}
	if existingID != "" {
		return "", fmt.Errorf("ory provider: identity already exists for email=%s", user.Email)
	}

	// [New] Check all custom login IDs for collisions
	for _, lid := range user.CustomLoginIDs {
		if lid == "" {
			continue
		}
		existing, err := o.findIdentityID(lid)
		if err != nil {
			return "", fmt.Errorf("ory provider: search identity failed for %s: %w", lid, err)
		}
		if existing != "" {
			return "", fmt.Errorf("ory provider: identifier %s already exists", lid)
		}
	}

	// [Legacy] check single LoginID
	if user.LoginID != "" {
		existingLoginID, err := o.findIdentityID(user.LoginID)
		if err != nil {
			return "", fmt.Errorf("ory provider: search identity failed: %w", err)
		}
		if existingLoginID != "" {
			return "", fmt.Errorf("ory provider: identity already exists for login_id=%s", user.LoginID)
		}
	}

	if user.PhoneNumber != "" {
		existingPhoneID, err := o.findIdentityID(user.PhoneNumber)
		if err != nil {
			return "", fmt.Errorf("ory provider: search identity failed: %w", err)
		}
		if existingPhoneID != "" {
			return "", fmt.Errorf("ory provider: identity already exists for phone=%s", user.PhoneNumber)
		}
	}

	traits := map[string]interface{}{
		"email": user.Email,
		"name":  user.Name,
	}
	if len(user.CustomLoginIDs) > 0 {
		traits["custom_login_ids"] = user.CustomLoginIDs
	} else if user.LoginID != "" {
		traits["custom_login_ids"] = []string{user.LoginID}
	}

	if user.PhoneNumber != "" {
		traits["phone_number"] = user.PhoneNumber
	}
	for k, v := range user.Attributes {
		// [SoT Fix] Don't let attributes overwrite core traits or use old 'id' trait
		if k == "id" || k == "email" || k == "custom_login_ids" {
			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,
				},
			},
		},
	}
	verifiable := []map[string]interface{}{
		{
			"value":    user.Email,
			"verified": true,
			"via":      "email",
		},
	}
	if user.PhoneNumber != "" {
		verifiable = append(verifiable, map[string]interface{}{
			"value":    user.PhoneNumber,
			"verified": true,
			"via":      "sms",
		})
	}
	payload["verifiable_addresses"] = verifiable
	payload["recovery_addresses"] = []map[string]interface{}{
		{
			"value": user.Email,
			"via":   "email",
		},
	}

	body, _ := json.Marshal(payload)
	req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, fmt.Sprintf("%s/admin/identities", o.KratosAdminURL), bytes.NewReader(body))
	if err != nil {
		return "", fmt.Errorf("ory provider: build create request failed: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := o.httpClient().Do(req)
	if err != nil {
		return "", fmt.Errorf("ory provider: create identity request failed: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return "", fmt.Errorf("ory provider: 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 "", fmt.Errorf("ory provider: decode create identity response failed: %w", err)
	}

	slog.Info("Ory identity created", "identity_id", created.ID, "email", user.Email)
	return created.ID, nil
}

// SignIn은 Kratos Public API의 login API 플로우를 사용해 세션 토큰을 발급합니다.
func (o *OryProvider) SignIn(loginID, password string) (*domain.AuthInfo, error) {
	if loginID == "" || password == "" {
		return nil, fmt.Errorf("ory provider: loginID and password are required")
	}

	flowID, err := o.startLoginFlow("")
	if err != nil {
		return nil, err
	}

	body, _ := json.Marshal(map[string]string{
		"identifier": loginID,
		"password":   password,
		"method":     "password",
	})
	loginURL := fmt.Sprintf("%s/self-service/login?flow=%s", o.KratosPublicURL, flowID)
	req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, loginURL, bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("ory provider: build login request failed: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

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

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

	var result struct {
		SessionToken          string    `json:"session_token"`
		SessionTokenExpiresAt time.Time `json:"session_token_expires_at"`
		Session               struct {
			ID       string `json:"id"`
			Identity struct {
				ID string `json:"id"`
			} `json:"identity"`
		} `json:"session"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("ory provider: decode login response failed: %w", err)
	}
	if result.SessionToken == "" {
		return nil, fmt.Errorf("ory provider: empty session token returned")
	}

	slog.Info("Ory login successful",
		"identity_id", result.Session.Identity.ID,
		"loginID", loginID,
		"expires_at", result.SessionTokenExpiresAt,
	)

	return &domain.AuthInfo{
		SessionToken: &domain.Token{
			JWT:        result.SessionToken,
			Expiration: result.SessionTokenExpiresAt,
			SessionID:  result.Session.ID,
		},
		Subject:    result.Session.Identity.ID,
		SetCookies: resp.Cookies(),
	}, nil
}

// UserExists는 Kratos Admin API로 loginID 존재 여부를 확인합니다.
func (o *OryProvider) UserExists(loginID string) (bool, error) {
	if loginID == "" {
		return false, fmt.Errorf("ory provider: loginID is empty")
	}
	identityID, err := o.findIdentityID(loginID)
	if err != nil {
		return false, fmt.Errorf("ory provider: find identity failed: %w", err)
	}
	return identityID != "", nil
}

// IssueSession은 Ory에서 별도 세션 발급이 필요할 때 사용합니다. (현재 미지원)
func (o *OryProvider) IssueSession(loginID string) (*domain.AuthInfo, error) {
	return nil, domain.ErrNotSupported
}

// InitiateLinkLogin은 Kratos Public API로 링크 로그인 플로우를 시작하고 이메일 전송을 트리거합니다.
func (o *OryProvider) InitiateLinkLogin(loginID, returnTo string) (*domain.LinkLoginInit, error) {
	if loginID == "" {
		return nil, fmt.Errorf("ory provider: loginID is required")
	}

	effectiveLoginID, err := o.resolveEffectiveLoginID(loginID)
	if err != nil {
		return nil, err
	}

	if err := o.ensureCodeLoginIdentifier(effectiveLoginID); err != nil {
		return nil, err
	}

	init, err := o.submitLoginCodeInit(effectiveLoginID, returnTo)
	if err == nil {
		init.LoginID = effectiveLoginID
		return init, nil
	}

	if shouldBootstrapCodeLogin(err) {
		if ensureErr := o.ensureCodeLoginIdentifier(effectiveLoginID); ensureErr == nil {
			init, initErr := o.submitLoginCodeInit(effectiveLoginID, returnTo)
			if initErr == nil {
				init.LoginID = effectiveLoginID
			}
			return init, initErr
		} else {
			slog.Warn("Ory code login bootstrap failed", "loginID", effectiveLoginID, "error", ensureErr)
		}
	}

	return nil, err
}

func (o *OryProvider) resolveEffectiveLoginID(loginID string) (string, error) {
	if strings.Contains(loginID, "@") {
		return loginID, nil
	}

	identityID, err := o.findIdentityID(loginID)
	if err != nil {
		return "", err
	}
	if identityID == "" {
		return "", fmt.Errorf("ory provider: identity not found for loginID=%s", loginID)
	}

	fullIdentity, err := o.fetchIdentityFull(identityID)
	if err != nil {
		return "", err
	}
	if fullIdentity != nil {
		if emailRaw, ok := fullIdentity.Traits["email"]; ok {
			if email, ok := emailRaw.(string); ok && email != "" {
				return email, nil
			}
		}
	}

	return "", fmt.Errorf("ory provider: email trait missing for loginID=%s", loginID)
}

func (o *OryProvider) submitLoginCodeInit(loginID, returnTo string) (*domain.LinkLoginInit, error) {
	flowID, err := o.startLoginFlow(returnTo)
	if err != nil {
		return nil, err
	}

	body, _ := json.Marshal(map[string]string{
		"method":     "code",
		"identifier": loginID,
	})
	loginURL := fmt.Sprintf("%s/self-service/login?flow=%s", o.KratosPublicURL, flowID)
	req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, loginURL, bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("ory provider: build link login request failed: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := o.httpClient().Do(req)
	if err != nil {
		return nil, fmt.Errorf("ory provider: link login request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
	if resp.StatusCode >= 300 {
		init, ok := parseKratosLinkLoginResponse(flowID, respBody)
		if ok {
			slog.Info("Ory link login initiated with non-2xx response", "loginID", loginID, "flow_id", flowID, "status", resp.StatusCode)
			return init, nil
		}
		return nil, fmt.Errorf("ory provider: link login failed status=%d body=%s", resp.StatusCode, string(respBody))
	}

	var result struct {
		ExpiresAt time.Time `json:"expires_at"`
	}
	_ = json.Unmarshal(respBody, &result)

	slog.Info("Ory link login initiated", "loginID", loginID, "flow_id", flowID)

	return &domain.LinkLoginInit{
		FlowID:    flowID,
		ExpiresAt: result.ExpiresAt,
		Mode:      "link",
	}, nil
}

func parseKratosLinkLoginResponse(flowID string, body []byte) (*domain.LinkLoginInit, bool) {
	if len(body) == 0 {
		return nil, false
	}
	var parsed struct {
		ExpiresAt time.Time `json:"expires_at"`
		State     string    `json:"state"`
		Active    string    `json:"active"`
	}
	if err := json.Unmarshal(body, &parsed); err != nil {
		return nil, false
	}
	state := strings.ToLower(parsed.State)
	active := strings.ToLower(parsed.Active)
	if strings.Contains(state, "sent") || active == "code" {
		return &domain.LinkLoginInit{
			FlowID:    flowID,
			ExpiresAt: parsed.ExpiresAt,
			Mode:      "link",
		}, true
	}
	return nil, false
}

func shouldBootstrapCodeLogin(err error) bool {
	if err == nil {
		return false
	}
	msg := strings.ToLower(err.Error())
	return strings.Contains(msg, "has not setup sign in with code") ||
		strings.Contains(msg, "4000035")
}

type kratosVerifiableAddress struct {
	Value    string `json:"value"`
	Via      string `json:"via"`
	Verified bool   `json:"verified"`
	Status   string `json:"status,omitempty"`
}

func (o *OryProvider) ensureCodeLoginIdentifier(loginID string) error {
	identityID, err := o.findIdentityID(loginID)
	if err != nil {
		return fmt.Errorf("ory provider: find identity failed: %w", err)
	}
	if identityID == "" {
		return fmt.Errorf("ory provider: identity not found for loginID=%s", loginID)
	}

	identity, err := o.fetchIdentity(identityID)
	if err != nil {
		return err
	}

	via := "sms"
	if strings.Contains(loginID, "@") {
		via = "email"
	}

	exists := false
	existingIndex := -1
	addresses := make([]kratosVerifiableAddress, 0, len(identity.VerifiableAddresses)+1)
	for idx, addr := range identity.VerifiableAddresses {
		addresses = append(addresses, kratosVerifiableAddress{
			Value:    addr.Value,
			Via:      addr.Via,
			Verified: addr.Verified,
			Status:   addr.Status,
		})
		if addr.Value == loginID && addr.Via == via {
			exists = true
			existingIndex = idx
		}
	}
	ops := make([]map[string]interface{}, 0, 2)
	if !exists {
		ops = append(ops, map[string]interface{}{
			"op":   "add",
			"path": "/verifiable_addresses/-",
			"value": map[string]interface{}{
				"value":    loginID,
				"via":      via,
				"verified": true,
				"status":   "completed",
			},
		})
	} else {
		addr := identity.VerifiableAddresses[existingIndex]
		if !addr.Verified {
			ops = append(ops, map[string]interface{}{
				"op":    "replace",
				"path":  fmt.Sprintf("/verifiable_addresses/%d/verified", existingIndex),
				"value": true,
			})
		}
		if addr.Status != "" && addr.Status != "completed" {
			ops = append(ops, map[string]interface{}{
				"op":    "replace",
				"path":  fmt.Sprintf("/verifiable_addresses/%d/status", existingIndex),
				"value": "completed",
			})
		}
	}

	if len(ops) == 0 {
		slog.Info("Ory identity verifiable address already ready", "identity_id", identityID, "loginID", loginID, "via", via)
		return nil
	}

	if err := o.patchIdentity(identityID, ops); err != nil {
		slog.Warn("Ory identity patch failed, trying full update", "identity_id", identityID, "error", err)
	}

	fullIdentity, err := o.fetchIdentityFull(identityID)
	if err != nil {
		return err
	}

	addresses = make([]kratosVerifiableAddress, 0, len(fullIdentity.VerifiableAddresses)+1)
	found := false
	for _, addr := range fullIdentity.VerifiableAddresses {
		addresses = append(addresses, kratosVerifiableAddress{
			Value:    addr.Value,
			Via:      addr.Via,
			Verified: addr.Verified,
			Status:   addr.Status,
		})
		if addr.Value == loginID && addr.Via == via {
			found = true
		}
	}
	if !found {
		addresses = append(addresses, kratosVerifiableAddress{
			Value:    loginID,
			Via:      via,
			Verified: true,
			Status:   "completed",
		})
	}

	payload := map[string]interface{}{
		"schema_id":            fullIdentity.SchemaID,
		"traits":               fullIdentity.Traits,
		"verifiable_addresses": addresses,
	}
	if len(fullIdentity.RecoveryAddresses) > 0 {
		payload["recovery_addresses"] = fullIdentity.RecoveryAddresses
	}

	body, _ := json.Marshal(payload)
	req, err := http.NewRequestWithContext(context.Background(), http.MethodPut, fmt.Sprintf("%s/admin/identities/%s", o.KratosAdminURL, identityID), bytes.NewReader(body))
	if err != nil {
		return fmt.Errorf("ory provider: build identity update failed: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := o.httpClient().Do(req)
	if err != nil {
		return fmt.Errorf("ory provider: identity update failed: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
		return fmt.Errorf("ory provider: identity update failed status=%d body=%s", resp.StatusCode, string(respBody))
	}

	slog.Info("Ory identity updated with verifiable address", "identity_id", identityID, "loginID", loginID, "via", via)
	return nil
}

type kratosIdentity struct {
	VerifiableAddresses []kratosVerifiableAddress `json:"verifiable_addresses"`
}

type kratosRecoveryAddress struct {
	Value string `json:"value"`
	Via   string `json:"via"`
}

type kratosIdentityFull struct {
	SchemaID            string                    `json:"schema_id"`
	Traits              map[string]interface{}    `json:"traits"`
	VerifiableAddresses []kratosVerifiableAddress `json:"verifiable_addresses"`
	RecoveryAddresses   []kratosRecoveryAddress   `json:"recovery_addresses"`
}

func (o *OryProvider) patchIdentity(identityID string, ops []map[string]interface{}) error {
	body, _ := json.Marshal(ops)
	req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/admin/identities/%s", o.KratosAdminURL, identityID), bytes.NewReader(body))
	if err != nil {
		return fmt.Errorf("ory provider: build identity patch failed: %w", err)
	}
	req.Header.Set("Content-Type", "application/json-patch+json")

	resp, err := o.httpClient().Do(req)
	if err != nil {
		return fmt.Errorf("ory provider: identity patch failed: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 300 {
		respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
		return fmt.Errorf("ory provider: identity patch failed status=%d body=%s", resp.StatusCode, string(respBody))
	}

	slog.Info("Ory identity patched", "identity_id", identityID, "ops", len(ops))
	return nil
}

func (o *OryProvider) fetchIdentity(identityID string) (*kratosIdentity, error) {
	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprintf("%s/admin/identities/%s", o.KratosAdminURL, identityID), nil)
	if err != nil {
		return nil, fmt.Errorf("ory provider: build identity get failed: %w", err)
	}

	resp, err := o.httpClient().Do(req)
	if err != nil {
		return nil, fmt.Errorf("ory provider: identity get failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
		return nil, fmt.Errorf("ory provider: identity get failed status=%d body=%s", resp.StatusCode, string(body))
	}

	var identity kratosIdentity
	if err := json.NewDecoder(resp.Body).Decode(&identity); err != nil {
		return nil, fmt.Errorf("ory provider: decode identity failed: %w", err)
	}
	return &identity, nil
}

func (o *OryProvider) fetchIdentityFull(identityID string) (*kratosIdentityFull, error) {
	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprintf("%s/admin/identities/%s", o.KratosAdminURL, identityID), nil)
	if err != nil {
		return nil, fmt.Errorf("ory provider: build identity get failed: %w", err)
	}

	resp, err := o.httpClient().Do(req)
	if err != nil {
		return nil, fmt.Errorf("ory provider: identity get failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
		return nil, fmt.Errorf("ory provider: identity get failed status=%d body=%s", resp.StatusCode, string(body))
	}

	var identity kratosIdentityFull
	if err := json.NewDecoder(resp.Body).Decode(&identity); err != nil {
		return nil, fmt.Errorf("ory provider: decode identity failed: %w", err)
	}
	return &identity, nil
}

// VerifyLoginCode는 Kratos 로그인 코드 제출로 세션을 발급합니다.
func (o *OryProvider) VerifyLoginCode(loginID, flowID, code string) (*domain.AuthInfo, error) {
	if loginID == "" || flowID == "" || code == "" {
		return nil, fmt.Errorf("ory provider: loginID, flowID and code are required")
	}

	body, _ := json.Marshal(map[string]string{
		"method":     "code",
		"identifier": loginID,
		"code":       code,
	})
	loginURL := fmt.Sprintf("%s/self-service/login?flow=%s", o.KratosPublicURL, flowID)
	req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, loginURL, bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("ory provider: build login code request failed: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := o.httpClient().Do(req)
	if err != nil {
		return nil, fmt.Errorf("ory provider: login code request failed: %w", err)
	}
	defer resp.Body.Close()

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

	var result struct {
		SessionToken          string    `json:"session_token"`
		SessionTokenExpiresAt time.Time `json:"session_token_expires_at"`
		Session               struct {
			ID       string `json:"id"`
			Identity struct {
				ID string `json:"id"`
			} `json:"identity"`
		} `json:"session"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("ory provider: decode login code response failed: %w", err)
	}
	if result.SessionToken == "" {
		return nil, fmt.Errorf("ory provider: empty session token returned")
	}

	slog.Info("Ory login code successful",
		"identity_id", result.Session.Identity.ID,
		"loginID", loginID,
		"expires_at", result.SessionTokenExpiresAt,
	)

	return &domain.AuthInfo{
		SessionToken: &domain.Token{
			JWT:        result.SessionToken,
			Expiration: result.SessionTokenExpiresAt,
			SessionID:  result.Session.ID,
		},
		Subject:    result.Session.Identity.ID,
		SetCookies: resp.Cookies(),
	}, nil
}

// GetPasswordPolicy는 Ory 환경에서 사용하는 기본 정책을 반환합니다.
func (o *OryProvider) GetPasswordPolicy() (*domain.PasswordPolicy, error) {
	return &domain.PasswordPolicy{
		MinLength:         12,
		Lowercase:         true,
		Uppercase:         false,
		Number:            true,
		NonAlphanumeric:   true,
		MinCharacterTypes: 0,
	}, nil
}

// InitiatePasswordReset는 현재 내부 토큰/메일 흐름을 사용하고 있으므로 NO-OP로 둡니다.
func (o *OryProvider) InitiatePasswordReset(loginID, redirectUrl string) error {
	slog.Info("Ory InitiatePasswordReset bypassed (handled by app internal flow)", "loginID", loginID, "redirect", redirectUrl)
	return nil
}

// VerifyPasswordResetToken는 내부 토큰 검증 흐름을 사용하므로 아직 구현하지 않습니다.
func (o *OryProvider) VerifyPasswordResetToken(token string) (*domain.AuthInfo, error) {
	return nil, fmt.Errorf("ory provider: VerifyPasswordResetToken not implemented (internal token flow expected)")
}

// UpdateUserPassword: Kratos Admin API를 통해 비밀번호를 갱신합니다.
func (o *OryProvider) UpdateUserPassword(loginID, newPassword string, r *http.Request) error {
	if loginID == "" || newPassword == "" {
		return fmt.Errorf("ory provider: loginID or new password missing")
	}

	identityID, err := o.findIdentityID(loginID)
	if err != nil {
		return fmt.Errorf("ory provider: find identity failed: %w", err)
	}

	if identityID == "" {
		return fmt.Errorf("ory provider: identity not found for loginID=%s", loginID)
	}

	identity, err := o.getIdentity(identityID)
	if err != nil {
		return fmt.Errorf("ory provider: load identity failed: %w", err)
	}
	if identity == nil {
		return fmt.Errorf("ory provider: identity payload missing for loginID=%s", loginID)
	}

	hashedPassword, err := hashPasswordForKratos(newPassword)
	if err != nil {
		return fmt.Errorf("ory provider: hash password failed: %w", 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)
	req, err := http.NewRequestWithContext(context.Background(), http.MethodPut, fmt.Sprintf("%s/admin/identities/%s", o.KratosAdminURL, identityID), bytes.NewReader(body))
	if err != nil {
		return fmt.Errorf("ory provider: build request failed: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

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

	slog.Info("Ory password updated via Kratos admin", "identity_id", identityID, "loginID", loginID)
	return nil
}

func getenv(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

// findIdentityID: Kratos Admin API에서 credentials_identifier로 검색 후 첫 번째 identity id 반환
func (o *OryProvider) findIdentityID(loginID string) (string, error) {
	u, err := url.Parse(fmt.Sprintf("%s/admin/identities", o.KratosAdminURL))
	if err != nil {
		return "", err
	}

	query := u.Query()
	query.Set("credentials_identifier", loginID)
	u.RawQuery = query.Encode()

	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, u.String(), nil)
	if err != nil {
		return "", err
	}

	resp, err := o.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, 1024))
		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 "", fmt.Errorf("decode response failed: %w", err)
	}
	if len(identities) == 0 {
		return "", nil
	}
	return identities[0].ID, nil
}

func (o *OryProvider) getIdentity(identityID string) (*KratosIdentity, error) {
	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprintf("%s/admin/identities/%s", o.KratosAdminURL, identityID), nil)
	if err != nil {
		return nil, err
	}

	resp, err := o.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 {
		respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
		return nil, fmt.Errorf("ory provider: get identity failed status=%d body=%s", resp.StatusCode, string(respBody))
	}

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

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

func (o *OryProvider) httpClient() *http.Client {
	if o.HTTPClient != nil {
		return o.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,
		},
	}
}

// startLoginFlow는 Kratos Public API에서 login flow ID를 발급받습니다.
func (o *OryProvider) startLoginFlow(returnTo string) (string, error) {
	loginURL := fmt.Sprintf("%s/self-service/login/api", o.KratosPublicURL)
	if returnTo != "" {
		loginURL = loginURL + "?return_to=" + url.QueryEscape(returnTo)
	}
	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, loginURL, nil)
	if err != nil {
		return "", fmt.Errorf("ory provider: build login flow request failed: %w", err)
	}

	resp, err := o.httpClient().Do(req)
	if err != nil {
		return "", fmt.Errorf("ory provider: login flow request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
		return "", fmt.Errorf("ory provider: login flow failed status=%d body=%s", resp.StatusCode, string(body))
	}

	var result struct {
		ID string `json:"id"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return "", fmt.Errorf("ory provider: decode login flow failed: %w", err)
	}
	if result.ID == "" {
		return "", fmt.Errorf("ory provider: empty login flow id")
	}
	return result.ID, nil
}
