package audit

import (
	"context"
	"database/sql"
	"strings"

	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
)

// Reader is the generic, app-blind owner of privileged audit-log reads. Route
// gates and product response projection remain outside this package.
type Reader struct{ db *sql.DB }

func NewReader(db *sql.DB) *Reader { return &Reader{db: db} }

func (reader *Reader) QueryAuditLog(
	ctx context.Context,
	request contract.AuditLogQueryRequest,
	_ *models.UserSession,
) (*contract.AuditLogQueryResult, error) {
	if reader == nil || reader.db == nil {
		return nil, auditLogReadError(contract.AuditLogErrorUnavailable, "audit log backend not initialized", nil)
	}
	where := []string{"ObjectApiName = ?"}
	args := []any{request.ObjectAPIName}
	if request.RecordID != "" {
		where, args = append(where, "RecordId = ?"), append(args, request.RecordID)
	}
	if request.ChangedByID != "" {
		where, args = append(where, "ChangedById = ?"), append(args, request.ChangedByID)
	}
	if request.FieldName != "" {
		where, args = append(where, "FieldName = ?"), append(args, request.FieldName)
	}
	if !request.Since.IsZero() {
		where, args = append(where, "ChangedAt >= ?"), append(args, request.Since.UTC())
	}
	if request.Outcome != "" {
		where, args = append(where, "Outcome = ?"), append(args, request.Outcome)
	}
	args = append(args, request.Limit, request.Offset)
	rows, err := reader.db.QueryContext(ctx,
		`SELECT Id, ObjectApiName, RecordId, FieldName, OldValue, NewValue,
		        ChangedById, ChangedAt, Action, ChangesJSON, Entrypoint, EntrypointDetail,
		        RequestId, TraceId, SessionId, ClientIp, ActualUserId,
		        Outcome, ErrorCode
		 FROM _System_AuditLog
		 WHERE `+strings.Join(where, " AND ")+`
		 ORDER BY ChangedAt DESC, Id DESC
		 LIMIT ? OFFSET ?`,
		args...,
	)
	if err != nil {
		return nil, auditLogReadError(contract.AuditLogErrorQueryFailed, "audit log query failed", err)
	}
	defer rows.Close()

	result := &contract.AuditLogQueryResult{
		Rows: make([]contract.AuditLogRow, 0, 32), Limit: request.Limit,
		Offset: request.Offset, Object: request.ObjectAPIName,
	}
	for rows.Next() {
		var row contract.AuditLogRow
		var oldValue, newValue, action, changesJSON, entrypoint, entrypointDetail sql.NullString
		var requestID, traceID, sessionID, clientIP, actualUserID, outcome, errorCode sql.NullString
		var changedAt sql.NullTime
		if err := rows.Scan(
			&row.ID, &row.ObjectAPIName, &row.RecordID, &row.FieldName,
			&oldValue, &newValue, &row.ChangedByID, &changedAt,
			&action, &changesJSON, &entrypoint, &entrypointDetail,
			&requestID, &traceID, &sessionID, &clientIP, &actualUserID,
			&outcome, &errorCode,
		); err != nil {
			return nil, auditLogReadError(contract.AuditLogErrorScanFailed, "audit log scan failed", err)
		}
		row.OldValue, row.NewValue = auditNullString(oldValue), auditNullString(newValue)
		if changedAt.Valid {
			row.ChangedAt, row.ChangedAtValid = changedAt.Time, true
		}
		row.Action, row.ChangesJSON = auditNullString(action), auditNullString(changesJSON)
		row.Entrypoint, row.EntrypointDetail = auditNullString(entrypoint), auditNullString(entrypointDetail)
		row.RequestID, row.TraceID = auditNullString(requestID), auditNullString(traceID)
		row.SessionID, row.ClientIP = auditNullString(sessionID), auditNullString(clientIP)
		row.ActualUserID, row.Outcome, row.ErrorCode = auditNullString(actualUserID), auditNullString(outcome), auditNullString(errorCode)
		result.Rows = append(result.Rows, row)
	}
	if err := rows.Err(); err != nil {
		return nil, auditLogReadError(contract.AuditLogErrorIterate, "audit log iterate failed", err)
	}
	return result, nil
}

func auditNullString(value sql.NullString) *string {
	if !value.Valid {
		return nil
	}
	copy := value.String
	return &copy
}

func auditLogReadError(kind contract.AuditLogErrorKind, message string, cause error) error {
	return &contract.AuditLogError{Kind: kind, Message: message, Cause: cause}
}

var _ contract.AuditLog = (*Reader)(nil)
