package sfcrmappservice

import (
	"context"
	"errors"
	"net/http"
	"strconv"
	"strings"

	crmwire "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/wire"
	auditsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/auditlogv1"
	permissionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/effectivepermissionsv1"
)

const auditLogPermissionViewFieldAuditTrail = "ViewFieldAuditTrail"

type RuntimeReadAuditLogFunc func(
	context.Context,
	string,
	auditsemantic.Request,
) (auditsemantic.Response, error)

type RuntimeAuditLogClient struct {
	readAuditLog    RuntimeReadAuditLogFunc
	readPermissions RuntimeReadEffectivePermissionsFunc
}

func NewRuntimeAuditLogClient(
	readAuditLog RuntimeReadAuditLogFunc,
	readPermissions RuntimeReadEffectivePermissionsFunc,
) (*RuntimeAuditLogClient, error) {
	if readAuditLog == nil || readPermissions == nil {
		return nil, errAuditLogUnavailable
	}
	return &RuntimeAuditLogClient{
		readAuditLog: readAuditLog, readPermissions: readPermissions,
	}, nil
}

type adminAuditLogRow struct {
	ID               string  `json:"Id"`
	ObjectAPIName    string  `json:"ObjectApiName"`
	RecordID         string  `json:"RecordId"`
	FieldName        string  `json:"FieldName"`
	OldValue         *string `json:"OldValue,omitempty"`
	NewValue         *string `json:"NewValue,omitempty"`
	ChangedByID      string  `json:"ChangedById"`
	ChangedAt        string  `json:"ChangedAt"`
	Action           string  `json:"Action,omitempty"`
	ChangesJSON      string  `json:"ChangesJSON,omitempty"`
	Entrypoint       string  `json:"Entrypoint,omitempty"`
	EntrypointDetail string  `json:"EntrypointDetail,omitempty"`
	RequestID        string  `json:"RequestId,omitempty"`
	TraceID          string  `json:"TraceId,omitempty"`
	SessionID        string  `json:"SessionId,omitempty"`
	ClientIP         string  `json:"ClientIp,omitempty"`
	ActualUserID     string  `json:"ActualUserId,omitempty"`
	Outcome          string  `json:"Outcome,omitempty"`
	ErrorCode        string  `json:"ErrorCode,omitempty"`
}

type adminAuditLogResponse struct {
	Records []adminAuditLogRow `json:"records"`
	Count   int                `json:"count"`
	Limit   int                `json:"limit"`
	Offset  int                `json:"offset"`
	Object  string             `json:"object"`
}

var errAuditLogUnavailable = errors.New("sfcrm AppService: Runtime audit log and effective-permission ports are required")

func matchAdminAuditLogRoute(path string) bool {
	parts := strings.Split(strings.Trim(path, "/"), "/")
	if len(parts) != 6 || parts[0] != "services" || parts[1] != "data" ||
		parts[3] != "admin" || parts[4] != "audit" || parts[5] != "log" {
		return false
	}
	_, supported := recordViewVersions[parts[2]]
	return supported
}

func (service *Service) serveAdminAuditLog(writer http.ResponseWriter, request *http.Request) bool {
	if service.auditLog == nil || !matchAdminAuditLogRoute(request.URL.EscapedPath()) {
		return false
	}
	service.requests.Add(1)
	if request.Method != http.MethodGet {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writer.Header().Set("Allow", http.MethodGet)
		writeCRMError(writer, http.StatusMethodNotAllowed, crmwire.SFCodeInvalidRequest, "Method not allowed")
		return true
	}
	if service.draining.Load() {
		service.drainRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service is draining")
		return true
	}
	bearer, ok := endUserBearer(request.Header.Values("Authorization"))
	if !ok {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(writer, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID, "Session expired or invalid")
		return true
	}
	service.inFlight.Add(1)
	defer service.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), service.requestTimeout)
	defer cancel()
	permissions, err := service.auditLog.readPermissions(ctx, bearer, systemPermissionAuthorityRequest())
	if err != nil {
		service.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return true
	}
	if err := permissionsemantic.ValidateResponse(permissions); err != nil {
		service.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return true
	}
	if !hasSystemPermission(permissions.SystemPermissions, auditLogPermissionViewFieldAuditTrail) {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(writer, http.StatusForbidden, crmwire.SFCodeForbidden, "Insufficient access")
		return true
	}
	objectID, recordID := request.URL.Query().Get("object"), request.URL.Query().Get("recordId")
	if objectID == "" || recordID == "" {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeJSON(writer, http.StatusBadRequest, map[string]any{"error": "object and recordId are required"})
		return true
	}
	limit := auditLogPositiveInt(request.URL.Query().Get("limit"), 100)
	if limit > auditsemantic.MaxRows {
		limit = auditsemantic.MaxRows
	}
	offset := auditLogNonnegativeInt(request.URL.Query().Get("offset"), 0)
	response, err := service.auditLog.readAuditLog(ctx, bearer, auditsemantic.Request{
		ObjectID: objectID, RecordID: recordID, Limit: uint32(limit), Offset: uint64(offset),
		MinCatalogRevision: &permissions.Receipts.CatalogRevision,
	})
	if err != nil {
		service.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return true
	}
	if err := auditsemantic.ValidateResponse(response); err != nil {
		service.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return true
	}
	if permissions.Receipts.CatalogRevision != response.Receipts.CatalogRevision ||
		permissions.Receipts.SchemaGeneration != response.Receipts.SchemaGeneration ||
		permissions.Receipts.EngineGeneration != response.Receipts.EngineGeneration ||
		permissions.Receipts.SecurityEpoch != response.Receipts.SecurityEpoch {
		service.failures.Add(1)
		writeCRMError(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service temporarily unavailable")
		return true
	}
	rows := make([]adminAuditLogRow, 0, len(response.Rows))
	for _, row := range response.Rows {
		rows = append(rows, adminAuditLogRow{
			ID: row.ID, ObjectAPIName: row.ObjectID, RecordID: row.RecordID,
			FieldName: row.FieldID, OldValue: row.OldValue, NewValue: row.NewValue,
			ChangedByID: row.ChangedByID, ChangedAt: row.ChangedAt,
			Action: row.Action, ChangesJSON: row.ChangesJSON, Entrypoint: row.Entrypoint,
			EntrypointDetail: row.EntrypointDetail, RequestID: row.RequestID,
			TraceID: row.TraceID, SessionID: row.SessionID, ClientIP: row.ClientIP,
			ActualUserID: row.ActualUserID, Outcome: row.Outcome, ErrorCode: row.ErrorCode,
		})
	}
	service.successes.Add(1)
	writeJSON(writer, http.StatusOK, adminAuditLogResponse{
		Records: rows, Count: len(rows), Limit: limit, Offset: offset, Object: objectID,
	})
	return true
}

func auditLogPositiveInt(raw string, fallback int) int {
	if value, err := strconv.Atoi(raw); err == nil && value > 0 {
		return value
	}
	return fallback
}

func auditLogNonnegativeInt(raw string, fallback int) int {
	if value, err := strconv.Atoi(raw); err == nil && value >= 0 {
		return value
	}
	return fallback
}
