package businessscope

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"
)

const (
	apiPageLimit    = 500
	apiMaxPages     = 100
	apiMaxRows      = 50000
	apiMaxBodyBytes = 8 << 20
	apiSourcePrefix = "oneos-api-v1:"
)

type APIConfig struct {
	URL           string
	ServiceToken  string
	SigningSecret string
	MaxAttempts   int
	HTTPClient    *http.Client
	Now           func() time.Time
}

type apiEnvelope struct {
	Code      int             `json:"code"`
	Message   string          `json:"message"`
	Data      apiSnapshotPage `json:"data"`
	RequestID string          `json:"requestId"`
}

type apiSnapshotPage struct {
	ScopeVersion string         `json:"scopeVersion"`
	GeneratedAt  string         `json:"generatedAt"`
	Complete     bool           `json:"complete"`
	NextCursor   string         `json:"nextCursor"`
	Items        []apiScopeItem `json:"items"`
	Rejected     []apiRejected  `json:"rejected"`
}

type apiScopeItem struct {
	VehicleID         json.RawMessage `json:"vehicleId"`
	VIN               string          `json:"vin"`
	PlateNumber       string          `json:"plateNumber"`
	CustomerID        json.RawMessage `json:"customerId"`
	CustomerName      string          `json:"customerName"`
	ContractID        json.RawMessage `json:"contractId"`
	ContractCode      string          `json:"contractCode"`
	ProjectName       string          `json:"projectName"`
	DepartmentID      json.RawMessage `json:"departmentId"`
	DepartmentName    string          `json:"departmentName"`
	ResponsibleUserID json.RawMessage `json:"responsibleUserId"`
	ResponsibleName   string          `json:"responsibleUserName"`
	OperationStatus   string          `json:"operationStatus"`
	ScopeStartAt      string          `json:"scopeStartAt"`
	SourceUpdatedAt   string          `json:"sourceUpdatedAt"`
}

type apiRejected struct {
	VehicleID  json.RawMessage `json:"vehicleId"`
	VIN        string          `json:"vin"`
	CustomerID json.RawMessage `json:"customerId"`
	ContractID json.RawMessage `json:"contractId"`
	Reason     string          `json:"reason"`
	ReasonCode string          `json:"reasonCode"`
}

func ReadAPISnapshot(ctx context.Context, config APIConfig) (Snapshot, error) {
	endpoint, err := url.Parse(strings.TrimSpace(config.URL))
	if err != nil || endpoint.Scheme == "" || endpoint.Host == "" {
		return Snapshot{}, fmt.Errorf("ONEOS_SCOPE_API_URL must be an absolute HTTP(S) URL")
	}
	if endpoint.Scheme != "https" && !isPrivateAPIHost(endpoint.Hostname()) {
		return Snapshot{}, fmt.Errorf("OneOS scope API must use HTTPS outside private networks")
	}
	if strings.TrimSpace(config.ServiceToken) == "" || strings.TrimSpace(config.SigningSecret) == "" {
		return Snapshot{}, fmt.Errorf("OneOS scope API service token and signing secret are required")
	}
	if config.MaxAttempts <= 0 {
		config.MaxAttempts = 3
	}
	if config.MaxAttempts > 5 {
		config.MaxAttempts = 5
	}
	if config.HTTPClient == nil {
		config.HTTPClient = &http.Client{Timeout: 10 * time.Second}
	}
	if config.Now == nil {
		config.Now = time.Now
	}

	candidates := make([]Candidate, 0, 1024)
	upstreamRejections := make([]Rejection, 0)
	cursor := ""
	seenCursors := map[string]bool{}
	scopeVersion := ""
	var generatedAt time.Time

	for pageNumber := 0; pageNumber < apiMaxPages; pageNumber++ {
		pageURL := *endpoint
		query := pageURL.Query()
		query.Set("limit", strconv.Itoa(apiPageLimit))
		if cursor != "" {
			query.Set("cursor", cursor)
		} else {
			query.Del("cursor")
		}
		pageURL.RawQuery = query.Encode()
		page, err := fetchAPIPage(ctx, config, &pageURL)
		if err != nil {
			return Snapshot{}, err
		}
		if !page.Complete {
			return Snapshot{}, fmt.Errorf("OneOS scope API returned an incomplete snapshot")
		}
		if strings.TrimSpace(page.ScopeVersion) == "" {
			return Snapshot{}, fmt.Errorf("OneOS scope API omitted scopeVersion")
		}
		pageGeneratedAt, err := time.Parse(time.RFC3339Nano, page.GeneratedAt)
		if err != nil {
			return Snapshot{}, fmt.Errorf("OneOS scope API generatedAt is invalid: %w", err)
		}
		if scopeVersion == "" {
			scopeVersion = page.ScopeVersion
			generatedAt = pageGeneratedAt
		} else if page.ScopeVersion != scopeVersion || !pageGeneratedAt.Equal(generatedAt) {
			return Snapshot{}, fmt.Errorf("OneOS scope API pagination changed snapshot version")
		}
		for _, item := range page.Items {
			candidate, err := candidateFromAPI(item, len(candidates)+len(upstreamRejections)+1)
			if err != nil {
				return Snapshot{}, err
			}
			candidates = append(candidates, candidate)
		}
		for _, rejected := range page.Rejected {
			upstreamRejections = append(upstreamRejections, rejectionFromAPI(rejected, len(candidates)+len(upstreamRejections)+1))
		}
		if len(candidates)+len(upstreamRejections) > apiMaxRows {
			return Snapshot{}, fmt.Errorf("OneOS scope API exceeded %d rows", apiMaxRows)
		}
		next := strings.TrimSpace(page.NextCursor)
		if next == "" {
			break
		}
		if seenCursors[next] {
			return Snapshot{}, fmt.Errorf("OneOS scope API repeated pagination cursor")
		}
		seenCursors[next] = true
		cursor = next
		if pageNumber == apiMaxPages-1 {
			return Snapshot{}, fmt.Errorf("OneOS scope API exceeded %d pages", apiMaxPages)
		}
	}

	snapshot, err := BuildSnapshot(candidates, generatedAt)
	if err != nil {
		return Snapshot{}, err
	}
	snapshot.Candidates += len(upstreamRejections)
	snapshot.Rejections = append(snapshot.Rejections, upstreamRejections...)
	versionHash := sha256.Sum256([]byte(scopeVersion + "\n" + snapshot.Checksum))
	snapshot.SourceVersion = apiSourcePrefix + hex.EncodeToString(versionHash[:])
	return snapshot, nil
}

func fetchAPIPage(ctx context.Context, config APIConfig, pageURL *url.URL) (apiSnapshotPage, error) {
	var lastErr error
	for attempt := 1; attempt <= config.MaxAttempts; attempt++ {
		requestID, err := randomHex(16)
		if err != nil {
			return apiSnapshotPage{}, err
		}
		timestamp := strconv.FormatInt(config.Now().UTC().Unix(), 10)
		canonical := http.MethodGet + "\n" + pageURL.RequestURI() + "\n" + timestamp + "\n" + requestID
		mac := hmac.New(sha256.New, []byte(config.SigningSecret))
		_, _ = mac.Write([]byte(canonical))
		request, err := http.NewRequestWithContext(ctx, http.MethodGet, pageURL.String(), nil)
		if err != nil {
			return apiSnapshotPage{}, err
		}
		request.Header.Set("Accept", "application/json")
		request.Header.Set("Authorization", "Service "+config.ServiceToken)
		request.Header.Set("X-Request-Id", requestID)
		request.Header.Set("X-Request-Timestamp", timestamp)
		request.Header.Set("X-Request-Signature", hex.EncodeToString(mac.Sum(nil)))
		response, err := config.HTTPClient.Do(request)
		if err != nil {
			lastErr = err
		} else {
			page, retry, responseErr := decodeAPIResponse(response)
			if responseErr == nil {
				return page, nil
			}
			lastErr = responseErr
			if !retry {
				return apiSnapshotPage{}, responseErr
			}
		}
		if attempt < config.MaxAttempts {
			select {
			case <-ctx.Done():
				return apiSnapshotPage{}, ctx.Err()
			case <-time.After(time.Duration(attempt*100) * time.Millisecond):
			}
		}
	}
	return apiSnapshotPage{}, fmt.Errorf("OneOS scope API failed after %d attempts: %w", config.MaxAttempts, lastErr)
}

func decodeAPIResponse(response *http.Response) (apiSnapshotPage, bool, error) {
	defer response.Body.Close()
	retry := response.StatusCode == http.StatusTooManyRequests || response.StatusCode >= 500
	if response.StatusCode != http.StatusOK {
		_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4096))
		return apiSnapshotPage{}, retry, fmt.Errorf("OneOS scope API returned HTTP %d", response.StatusCode)
	}
	var envelope apiEnvelope
	decoder := json.NewDecoder(io.LimitReader(response.Body, apiMaxBodyBytes+1))
	if err := decoder.Decode(&envelope); err != nil {
		return apiSnapshotPage{}, false, fmt.Errorf("decode OneOS scope API response: %w", err)
	}
	if envelope.Code != 0 {
		return apiSnapshotPage{}, false, fmt.Errorf("OneOS scope API returned business code %d", envelope.Code)
	}
	return envelope.Data, false, nil
}

func candidateFromAPI(item apiScopeItem, row int) (Candidate, error) {
	vehicleID, err := requiredInt64ID(item.VehicleID, "vehicleId")
	if err != nil {
		return Candidate{}, err
	}
	customerID, err := requiredInt64ID(item.CustomerID, "customerId")
	if err != nil {
		return Candidate{}, err
	}
	contractID, err := requiredInt64ID(item.ContractID, "contractId")
	if err != nil {
		return Candidate{}, err
	}
	scopeStart, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(item.ScopeStartAt))
	if err != nil {
		return Candidate{}, fmt.Errorf("OneOS scope API item %d has invalid scopeStartAt", row)
	}
	var sourceUpdatedAt *time.Time
	if value := strings.TrimSpace(item.SourceUpdatedAt); value != "" {
		parsed, err := time.Parse(time.RFC3339Nano, value)
		if err != nil {
			return Candidate{}, fmt.Errorf("OneOS scope API item %d has invalid sourceUpdatedAt", row)
		}
		sourceUpdatedAt = &parsed
	}
	return Candidate{
		RowNumber: row, VehicleID: vehicleID, VehiclePresent: true,
		VIN: item.VIN, PlateNumber: item.PlateNumber,
		CustomerID: customerID, CustomerName: item.CustomerName, CustomerPresent: true, CustomerProfileExists: true,
		ContractID: contractID, ContractPresent: true, ContractProfileExists: true,
		EffectiveCustomerID: customerID, EffectiveCustomerSet: true,
		ContractCode: item.ContractCode, ProjectName: item.ProjectName,
		DepartmentID: rawIDString(item.DepartmentID), DepartmentName: item.DepartmentName,
		ResponsibleUserID: rawIDString(item.ResponsibleUserID), ResponsibleUserName: item.ResponsibleName,
		OperationStatus: item.OperationStatus, ScopeStartAt: scopeStart, SourceUpdatedAt: sourceUpdatedAt,
	}, nil
}

func rejectionFromAPI(item apiRejected, row int) Rejection {
	reason := strings.ToUpper(strings.TrimSpace(firstNonEmptyScope(item.ReasonCode, item.Reason)))
	if reason == "" || len(reason) > 64 {
		reason = "UPSTREAM_REJECTED"
	}
	rejection := Rejection{RowNumber: row, VIN: normalizeVIN(item.VIN), ReasonCode: reason}
	if value, err := optionalInt64ID(item.VehicleID); err == nil && value > 0 {
		rejection.VehicleID = int64Pointer(value)
	}
	if value, err := optionalInt64ID(item.CustomerID); err == nil && value > 0 {
		rejection.CustomerID = int64Pointer(value)
	}
	if value, err := optionalInt64ID(item.ContractID); err == nil && value > 0 {
		rejection.ContractID = int64Pointer(value)
	}
	return rejection
}

func requiredInt64ID(raw json.RawMessage, name string) (int64, error) {
	value, err := optionalInt64ID(raw)
	if err != nil || value <= 0 {
		return 0, fmt.Errorf("OneOS scope API %s must be a positive integer string", name)
	}
	return value, nil
}

func optionalInt64ID(raw json.RawMessage) (int64, error) {
	value := rawIDString(raw)
	if value == "" {
		return 0, nil
	}
	return strconv.ParseInt(value, 10, 64)
}

func rawIDString(raw json.RawMessage) string {
	value := strings.TrimSpace(string(raw))
	if value == "" || value == "null" {
		return ""
	}
	if strings.HasPrefix(value, `"`) {
		var decoded string
		if json.Unmarshal(raw, &decoded) == nil {
			return strings.TrimSpace(decoded)
		}
		return ""
	}
	return value
}

func isPrivateAPIHost(host string) bool {
	host = strings.ToLower(strings.TrimSpace(host))
	if host == "localhost" {
		return true
	}
	address := net.ParseIP(host)
	return address != nil && (address.IsPrivate() || address.IsLoopback())
}

func firstNonEmptyScope(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return value
		}
	}
	return ""
}
