// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package object

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

	"github.com/casdoor/casdoor/util"
)

// SCIMSyncerProvider implements SyncerProvider for SCIM 2.0 API-based syncers
type SCIMSyncerProvider struct {
	Syncer *Syncer
}

// InitAdapter initializes the SCIM syncer (no database adapter needed)
func (p *SCIMSyncerProvider) InitAdapter() error {
	// SCIM syncer doesn't need database adapter
	return nil
}

// GetOriginalUsers retrieves all users from SCIM API
func (p *SCIMSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
	return p.getSCIMUsers()
}

// AddUser adds a new user to SCIM (not supported for read-only API)
func (p *SCIMSyncerProvider) AddUser(user *OriginalUser) (bool, error) {
	// SCIM syncer is typically read-only
	return false, fmt.Errorf("adding users to SCIM is not supported")
}

// UpdateUser updates an existing user in SCIM (not supported for read-only API)
func (p *SCIMSyncerProvider) UpdateUser(user *OriginalUser) (bool, error) {
	// SCIM syncer is typically read-only
	return false, fmt.Errorf("updating users in SCIM is not supported")
}

// TestConnection tests the SCIM API connection
func (p *SCIMSyncerProvider) TestConnection() error {
	// Test by trying to fetch users with a limit of 1
	endpoint := p.buildSCIMEndpoint()
	endpoint = fmt.Sprintf("%s?startIndex=1&count=1", endpoint)

	req, err := p.createSCIMRequest("GET", endpoint, nil)
	if err != nil {
		return err
	}

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("SCIM connection test failed: status=%d, body=%s", resp.StatusCode, string(body))
	}

	return nil
}

// Close closes any open connections (no-op for SCIM API-based syncer)
func (p *SCIMSyncerProvider) Close() error {
	// SCIM syncer doesn't maintain persistent connections
	return nil
}

// SCIMName represents a SCIM user name structure
type SCIMName struct {
	FamilyName string `json:"familyName"`
	GivenName  string `json:"givenName"`
	Formatted  string `json:"formatted"`
}

// SCIMEmail represents a SCIM user email structure
type SCIMEmail struct {
	Value   string `json:"value"`
	Type    string `json:"type"`
	Primary bool   `json:"primary"`
}

// SCIMPhoneNumber represents a SCIM user phone number structure
type SCIMPhoneNumber struct {
	Value   string `json:"value"`
	Type    string `json:"type"`
	Primary bool   `json:"primary"`
}

// SCIMAddress represents a SCIM user address structure
type SCIMAddress struct {
	StreetAddress string `json:"streetAddress"`
	Locality      string `json:"locality"`
	Region        string `json:"region"`
	PostalCode    string `json:"postalCode"`
	Country       string `json:"country"`
	Formatted     string `json:"formatted"`
	Type          string `json:"type"`
	Primary       bool   `json:"primary"`
}

// SCIMUser represents a SCIM 2.0 user resource
type SCIMUser struct {
	ID           string            `json:"id"`
	ExternalID   string            `json:"externalId"`
	UserName     string            `json:"userName"`
	Name         SCIMName          `json:"name"`
	DisplayName  string            `json:"displayName"`
	NickName     string            `json:"nickName"`
	ProfileURL   string            `json:"profileUrl"`
	Title        string            `json:"title"`
	UserType     string            `json:"userType"`
	PreferredLan string            `json:"preferredLanguage"`
	Locale       string            `json:"locale"`
	Timezone     string            `json:"timezone"`
	Active       bool              `json:"active"`
	Emails       []SCIMEmail       `json:"emails"`
	PhoneNumbers []SCIMPhoneNumber `json:"phoneNumbers"`
	Addresses    []SCIMAddress     `json:"addresses"`
}

// SCIMListResponse represents a SCIM list response
type SCIMListResponse struct {
	TotalResults int         `json:"totalResults"`
	ItemsPerPage int         `json:"itemsPerPage"`
	StartIndex   int         `json:"startIndex"`
	Resources    []*SCIMUser `json:"Resources"`
}

// buildSCIMEndpoint builds the SCIM API endpoint URL
func (p *SCIMSyncerProvider) buildSCIMEndpoint() string {
	// syncer.Host should be the SCIM server URL (e.g., https://example.com/scim/v2)
	host := strings.TrimSuffix(p.Syncer.Host, "/")
	return fmt.Sprintf("%s/Users", host)
}

// createSCIMRequest creates an HTTP request with proper authentication
func (p *SCIMSyncerProvider) createSCIMRequest(method, url string, body io.Reader) (*http.Request, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, method, url, body)
	if err != nil {
		return nil, err
	}

	// Set SCIM headers
	req.Header.Set("Content-Type", "application/scim+json")
	req.Header.Set("Accept", "application/scim+json")

	// Add authentication
	// syncer.User should be the authentication token or username
	// syncer.Password should be the password or API key
	if p.Syncer.User != "" && p.Syncer.Password != "" {
		// Try Basic Auth
		req.SetBasicAuth(p.Syncer.User, p.Syncer.Password)
	} else if p.Syncer.Password != "" {
		// Try Bearer token (assuming password field contains the token)
		req.Header.Set("Authorization", "Bearer "+p.Syncer.Password)
	} else if p.Syncer.User != "" {
		// Try Bearer token (assuming user field contains the token)
		req.Header.Set("Authorization", "Bearer "+p.Syncer.User)
	}

	return req, nil
}

// getSCIMUsers retrieves all users from SCIM API with pagination
func (p *SCIMSyncerProvider) getSCIMUsers() ([]*OriginalUser, error) {
	allUsers := []*SCIMUser{}
	startIndex := 1
	count := 100 // Fetch 100 users per page

	for {
		endpoint := p.buildSCIMEndpoint()
		endpoint = fmt.Sprintf("%s?startIndex=%d&count=%d", endpoint, startIndex, count)

		req, err := p.createSCIMRequest("GET", endpoint, nil)
		if err != nil {
			return nil, err
		}

		client := &http.Client{Timeout: 30 * time.Second}
		resp, err := client.Do(req)
		if err != nil {
			return nil, err
		}
		defer resp.Body.Close()

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

		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return nil, err
		}

		var listResp SCIMListResponse
		err = json.Unmarshal(body, &listResp)
		if err != nil {
			return nil, err
		}

		allUsers = append(allUsers, listResp.Resources...)

		// Check if we've fetched all users
		if len(allUsers) >= listResp.TotalResults {
			break
		}

		// Move to the next page
		startIndex += count
	}

	// Convert SCIM users to Casdoor OriginalUser
	originalUsers := []*OriginalUser{}
	for _, scimUser := range allUsers {
		originalUser := p.scimUserToOriginalUser(scimUser)
		originalUsers = append(originalUsers, originalUser)
	}

	return originalUsers, nil
}

// scimUserToOriginalUser converts SCIM user to Casdoor OriginalUser
func (p *SCIMSyncerProvider) scimUserToOriginalUser(scimUser *SCIMUser) *OriginalUser {
	user := &OriginalUser{
		Id:          scimUser.ID,
		ExternalId:  scimUser.ExternalID,
		Name:        scimUser.UserName,
		DisplayName: scimUser.DisplayName,
		FirstName:   scimUser.Name.GivenName,
		LastName:    scimUser.Name.FamilyName,
		Title:       scimUser.Title,
		Language:    scimUser.PreferredLan,
		Address:     []string{},
		Properties:  map[string]string{},
		Groups:      []string{},
	}

	// If display name is from name structure
	if user.DisplayName == "" && scimUser.Name.Formatted != "" {
		user.DisplayName = scimUser.Name.Formatted
	}

	// If display name is still empty, construct from first and last name
	if user.DisplayName == "" && (user.FirstName != "" || user.LastName != "") {
		user.DisplayName = strings.TrimSpace(fmt.Sprintf("%s %s", user.FirstName, user.LastName))
	}

	// Extract primary email or first email
	if len(scimUser.Emails) > 0 {
		for _, email := range scimUser.Emails {
			if email.Primary {
				user.Email = email.Value
				break
			}
		}
		// If no primary email, use the first one
		if user.Email == "" && len(scimUser.Emails) > 0 {
			user.Email = scimUser.Emails[0].Value
		}
	}

	// Extract primary phone or first phone
	if len(scimUser.PhoneNumbers) > 0 {
		for _, phone := range scimUser.PhoneNumbers {
			if phone.Primary {
				user.Phone = phone.Value
				break
			}
		}
		// If no primary phone, use the first one
		if user.Phone == "" && len(scimUser.PhoneNumbers) > 0 {
			user.Phone = scimUser.PhoneNumbers[0].Value
		}
	}

	// Extract primary address or first address
	if len(scimUser.Addresses) > 0 {
		for _, addr := range scimUser.Addresses {
			if addr.Primary {
				if addr.Formatted != "" {
					user.Address = []string{addr.Formatted}
				} else {
					user.Address = []string{addr.StreetAddress, addr.Locality, addr.Region, addr.PostalCode, addr.Country}
				}
				user.Location = addr.Locality
				user.Region = addr.Region
				break
			}
		}
		// If no primary address, use the first one
		if len(user.Address) == 0 && len(scimUser.Addresses) > 0 {
			addr := scimUser.Addresses[0]
			if addr.Formatted != "" {
				user.Address = []string{addr.Formatted}
			} else {
				user.Address = []string{addr.StreetAddress, addr.Locality, addr.Region, addr.PostalCode, addr.Country}
			}
			user.Location = addr.Locality
			user.Region = addr.Region
		}
	}

	// Set IsForbidden based on Active status
	user.IsForbidden = !scimUser.Active

	// Set CreatedTime to current time if not set
	if user.CreatedTime == "" {
		user.CreatedTime = util.GetCurrentTime()
	}

	return user
}

// GetOriginalGroups retrieves all groups from SCIM (not implemented yet)
func (p *SCIMSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
	// TODO: Implement SCIM group sync
	return []*OriginalGroup{}, nil
}

// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
func (p *SCIMSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
	// TODO: Implement SCIM user group membership sync
	return []string{}, nil
}
