package dialectsfcrm

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/nexuscrm/dialect-sf-crm/dashboard"
	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

const (
	dashboardDefaultLayoutType             = "FixedLayout"
	dashboardExecutionCacheTTL             = 5 * time.Minute
	dashboardExecutionCacheCleanupInterval = time.Minute
	dashboardDeveloperNameMaxBytes         = 80
	dashboardGenericInternalErrorMessage   = "An internal server error occurred. Contact your system administrator."
)

var (
	dashboardNonDeveloperNameChars = regexp.MustCompile(`[^a-zA-Z0-9_]`)
	dashboardRepeatedUnderscores   = regexp.MustCompile(`_+`)
	dashboardRefreshCache          = &dashboardResultCache{entries: make(map[string]*cachedDashboardResult)}
)

type dashboardResultCache struct {
	sync.RWMutex
	cleanupOnce sync.Once
	entries     map[string]*cachedDashboardResult
}

type cachedDashboardResult struct {
	result    *dashboard.Result
	timestamp time.Time
}

type dashboardResponse struct {
	ID            string `json:"id"`
	Name          string `json:"name"`
	Label         string `json:"label"`
	DeveloperName string `json:"developerName"`
	FolderID      string `json:"folderId,omitempty"`
	Description   string `json:"description,omitempty"`
	LayoutType    string `json:"layoutType,omitempty"`
}

type dashboardComponentRequest struct {
	ReportID string                 `json:"reportId"`
	Type     string                 `json:"type"`
	Title    string                 `json:"title"`
	Config   map[string]interface{} `json:"config,omitempty"`
}

type createDashboardRequest struct {
	Name          string                      `json:"name"`
	DeveloperName string                      `json:"developerName,omitempty"`
	FolderID      string                      `json:"folderId"`
	Description   string                      `json:"description,omitempty"`
	LayoutType    string                      `json:"layoutType,omitempty"`
	Components    []dashboardComponentRequest `json:"components,omitempty"`
}

type updateDashboardRequest struct {
	Name        *string `json:"name,omitempty"`
	Description *string `json:"description,omitempty"`
	LayoutType  *string `json:"layoutType,omitempty"`
}

func ListDashboardsNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	query := deps.Contracts.Query
	if query == nil {
		return dashboardInternalErrorResponse(), nil
	}
	limit := 200
	if raw := queryValue(req, "limit"); raw != "" {
		if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 && parsed <= 2000 {
			limit = parsed
		}
	}
	result, err := query.Query(ctx, queryspec.QuerySpec{
		ObjectAPIName: constants.TableDashboard,
		SortField:     "Name",
		SortDirection: "ASC",
		Limit:         limit,
	}, req.User)
	if err != nil {
		if resp := respondServiceError(req, err); resp != nil {
			return resp, nil
		}
		return internalErrorResponse(dashboardGenericInternalErrorMessage), nil
	}
	records := []contract.Record(nil)
	if result != nil {
		records = result.Records
	}
	dashboards := make([]dashboardResponse, 0, len(records))
	for _, record := range records {
		dashboards = append(dashboards, dashboardResponseFromRecord(record))
	}
	return &dialect.Response{Status: http.StatusOK, Body: map[string]interface{}{
		"dashboards": dashboards,
		"totalSize":  len(dashboards),
	}}, nil
}

func GetDashboardNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	svc, resp := dashboardServiceFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	dashboardID, resp := dashboardIDParam(req)
	if resp != nil {
		return resp, nil
	}
	def, err := svc.GetDashboard(ctx, dashboardID, req.User)
	if err != nil {
		return dashboardErrorResponse(req, err), nil
	}
	return &dialect.Response{Status: http.StatusOK, Body: map[string]interface{}{
		"dashboardMetadata": def,
		"componentData":     map[string]interface{}{},
	}}, nil
}

func CreateDashboardNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	records := deps.Contracts.Records
	if records == nil {
		return dashboardInternalErrorResponse(), nil
	}
	var body createDashboardRequest
	if err := json.NewDecoder(bytes.NewReader(req.Body)).Decode(&body); err != nil {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeRequiredFieldMissing, err.Error())}, nil
	}
	if strings.TrimSpace(body.Name) == "" {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeRequiredFieldMissing, "name is required")}, nil
	}
	if strings.TrimSpace(body.FolderID) == "" {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeRequiredFieldMissing, "folderId is required")}, nil
	}
	folderAccess := deps.Contracts.FolderAccess
	if folderAccess == nil {
		return dashboardInternalErrorResponse(), nil
	}
	if !folderAccess.CheckFolderAccess(ctx, req.User, body.FolderID, contract.FolderAccessManage) {
		return forbiddenResponse("You do not have Manage access on the target folder"), nil
	}
	developerName := body.DeveloperName
	if developerName == "" {
		developerName = sanitizeDashboardDeveloperName(body.Name)
	}
	layoutType := body.LayoutType
	if layoutType == "" {
		layoutType = dashboardDefaultLayoutType
	}
	data := contract.Record{
		"Name":          body.Name,
		"DeveloperName": developerName,
		"FolderId":      body.FolderID,
		"LayoutType":    layoutType,
	}
	if body.Description != "" {
		data["Description"] = body.Description
	}
	created, err := records.Insert(ctx, constants.TableDashboard, data, req.User)
	if err != nil {
		return writeErrorResponse(req, err, wire.SFCodeInsertFailed), nil
	}
	dashboardID := dashboardRecordString(created, "Id")
	for _, comp := range body.Components {
		compData := contract.Record{
			"DashboardId": dashboardID,
			"ReportId":    comp.ReportID,
			"Type":        comp.Type,
			"Title":       comp.Title,
		}
		if len(comp.Config) > 0 {
			if configJSON, err := json.Marshal(comp.Config); err == nil {
				compData["Config"] = string(configJSON)
			}
		}
		if _, err := records.Insert(ctx, constants.TableDashboardComponent, compData, req.User); err != nil {
			return writeErrorResponse(req, err, wire.SFCodeInsertFailed), nil
		}
	}
	return &dialect.Response{Status: http.StatusCreated, Body: dashboardResponseFromRecord(created)}, nil
}

func UpdateDashboardNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	records := deps.Contracts.Records
	if records == nil {
		return dashboardInternalErrorResponse(), nil
	}
	dashboardID, resp := dashboardIDParam(req)
	if resp != nil {
		return resp, nil
	}
	var body updateDashboardRequest
	if err := json.NewDecoder(bytes.NewReader(req.Body)).Decode(&body); err != nil {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidField, err.Error())}, nil
	}
	updates := contract.Record{}
	if body.Name != nil {
		updates["Name"] = *body.Name
	}
	if body.Description != nil {
		updates["Description"] = *body.Description
	}
	if body.LayoutType != nil {
		updates["LayoutType"] = *body.LayoutType
	}
	if len(updates) == 0 {
		return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidField, "No fields to update")}, nil
	}
	if err := records.Update(ctx, constants.TableDashboard, dashboardID, updates, req.User); err != nil {
		return writeErrorResponse(req, err, wire.SFCodeUpdateFailed), nil
	}
	record, err := records.Retrieve(ctx, constants.TableDashboard, dashboardID, req.User)
	if err != nil {
		return dashboardRecordRetrieveErrorResponse(req, err), nil
	}
	return &dialect.Response{Status: http.StatusOK, Body: dashboardResponseFromRecord(record)}, nil
}

func DeleteDashboardNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	records := deps.Contracts.Records
	if records == nil {
		return dashboardInternalErrorResponse(), nil
	}
	dashboardID, resp := dashboardIDParam(req)
	if resp != nil {
		return resp, nil
	}
	if err := records.Delete(ctx, constants.TableDashboard, dashboardID, req.User); err != nil {
		return writeErrorResponse(req, err, wire.SFCodeDeleteFailed), nil
	}
	return &dialect.Response{Status: http.StatusNoContent}, nil
}

func RefreshDashboardNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	svc, resp := dashboardServiceFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	dashboardID, resp := dashboardIDParam(req)
	if resp != nil {
		return resp, nil
	}
	filters := map[string]interface{}{}
	if startDate := queryValue(req, "startDate"); startDate != "" {
		filters["startDate"] = startDate
	}
	if endDate := queryValue(req, "endDate"); endDate != "" {
		filters["endDate"] = endDate
	}
	result, err := svc.RunDashboard(ctx, dashboardID, req.User, filters)
	if err != nil {
		return dashboardErrorResponse(req, err), nil
	}
	return &dialect.Response{Status: http.StatusOK, Body: dashboardResultWireBody(ctx, deps, result)}, nil
}

func RefreshDashboardPUTNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	svc, resp := dashboardServiceFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	dashboardID, resp := dashboardIDParam(req)
	if resp != nil {
		return resp, nil
	}
	result, err := svc.RunDashboard(ctx, dashboardID, req.User, nil)
	if err != nil {
		return dashboardErrorResponse(req, err), nil
	}
	dashboardRefreshCache.store(dashboardID, result)
	return &dialect.Response{Status: http.StatusOK, Body: map[string]interface{}{
		"statusUrl": dashboardStatusURL(req.Version, dashboardID),
	}}, nil
}

func GetDashboardStatusNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	svc, resp := dashboardServiceFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	dashboardID, resp := dashboardIDParam(req)
	if resp != nil {
		return resp, nil
	}
	cached, status := dashboardRefreshCache.get(dashboardID)
	if cached == nil {
		def, _ := svc.GetDashboard(ctx, dashboardID, req.User)
		return &dialect.Response{Status: http.StatusOK, Body: map[string]interface{}{
			"status":            status,
			"dashboardMetadata": def,
			"componentData":     map[string]interface{}{},
			"errorMessage":      dashboardStatusErrorMessage(status),
		}}, nil
	}
	return &dialect.Response{Status: http.StatusOK, Body: map[string]interface{}{
		"status":            "Success",
		"dashboardMetadata": cached.result.DashboardMetadata,
		"componentData":     dashboardComponentData(ctx, deps, cached.result.ComponentData),
	}}, nil
}

func dashboardResultWireBody(ctx context.Context, deps dialect.Deps, result *dashboard.Result) map[string]any {
	body := map[string]any{
		"dashboardMetadata": dashboard.Definition{},
		"componentData":     map[string]any{},
	}
	if result == nil {
		return body
	}
	body["dashboardMetadata"] = result.DashboardMetadata
	body["componentData"] = dashboardComponentData(ctx, deps, result.ComponentData)
	if !result.GeneratedAt.IsZero() {
		body["generatedAt"] = result.GeneratedAt
	}
	return body
}

func dashboardComponentData(ctx context.Context, deps dialect.Deps, componentData map[string]*models.ReportResult) map[string]any {
	if len(componentData) == 0 {
		return map[string]any{}
	}
	adapter := NewReportWireAdapter()
	schemas := make(map[string]*models.ObjectMetadata)
	projected := make(map[string]any, len(componentData))
	for componentID, result := range componentData {
		var schema *models.ObjectMetadata
		if result != nil {
			sourceObject := result.ReportMetadata.SourceObject
			if sourceObject != "" && deps.Contracts.Metadata != nil {
				if cachedSchema, ok := schemas[sourceObject]; ok {
					schema = cachedSchema
				} else if described, err := deps.Contracts.Metadata.Describe(ctx, sourceObject); err == nil {
					schema = described
					schemas[sourceObject] = described
				}
			}
		}
		projected[componentID] = adapter.ProjectReportResult(result, schema)
	}
	return projected
}

func dashboardServiceFromDeps(deps dialect.Deps) (*dashboard.Service, *dialect.Response) {
	if deps.Contracts.Query == nil || deps.Contracts.Reports == nil || deps.Contracts.UserSessions == nil {
		return nil, dashboardInternalErrorResponse()
	}
	return dashboard.NewService(
		deps.Contracts.Query,
		deps.Contracts.Reports,
		dashboard.WithDashboardEffectiveUserPolicy(
			dashboard.NewEffectiveUserPolicy(deps.Contracts.UserSessions, nil),
		),
	), nil
}

func dashboardIDParam(req *dialect.Request) (string, *dialect.Response) {
	id := strings.TrimSpace(req.Params["dashboardId"])
	if id == "" {
		return "", &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, "dashboardId is required")}
	}
	// The refresh response hands the client a statusUrl built from this id, and the CRM frontend
	// polls it directly (analytics.ts calls apiClient.get on whatever it was given). So this value
	// decides which surface an authenticated browser request lands on, which makes it the server's
	// job to keep it inside one path segment rather than the client's job to notice.
	//
	// A shape rule, not a naming convention: real ids here include hyphens and plain names
	// (01Z-dashboard, dash-1, Executive), so requiring a DeveloperName would refuse working input.
	if !addressablePathSegment(id) {
		return "", &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, "dashboardId is not valid")}
	}
	return id, nil
}

// dashboardStatusURL spells the poll link a refresh hands back.
//
// It has a name because the client follows it: analytics.ts polls whatever this says by handing it
// straight to apiClient.get, so this is the one place that decides which surface that authenticated
// request reaches. Its id argument must already have passed dashboardIDParam.
func dashboardStatusURL(apiVersion, dashboardID string) string {
	return fmt.Sprintf(
		"/services/data/%s/analytics/dashboards/%s/status",
		apiVersionOrDefault(apiVersion), dashboardID,
	)
}

// addressablePathSegment reports whether a value can be interpolated into a URL a client will follow
// without changing which surface the request reaches.
//
// This allows a set rather than forbidding one, on purpose. The first version listed the characters
// that break a path — `/`, `?`, `#`, `%` — and review immediately found two more classes it had
// missed: a backslash, which a browser's URL parser turns into a path separator, so `..\..\composite`
// escapes the class; and tab, CR and LF, which that parser strips before parsing, so the path a client
// sends is not the path this emitted. A list of what to forbid is a list someone extends every round.
//
// The unreserved set from RFC 3986 has no such characters by construction: it needs no escaping, no
// normalisation, and it means the same thing to every parser. It covers every id shape this deployment
// uses — 01Z-dashboard, 01Zxx0000000003AAA, Executive, dash-1 — so nothing working is refused.
//
// Escaping is still not an alternative to a shape rule. A percent-encoded separator is decoded back
// into net/http's URL.Path before any router sees it, which is why `%` is refused rather than relied
// upon, and why the dot-segment check stays: `.` and `..` are unreserved characters that a client
// resolves away.
func addressablePathSegment(value string) bool {
	if value == "" || value == "." || value == ".." {
		return false
	}
	for _, r := range value {
		switch {
		case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9':
		case r == '-', r == '.', r == '_', r == '~':
		default:
			return false
		}
	}
	return true
}

func dashboardErrorResponse(req *dialect.Request, err error) *dialect.Response {
	if resp := respondServiceError(req, err); resp != nil {
		return resp
	}
	return dashboardInternalErrorResponse()
}

func dashboardRecordRetrieveErrorResponse(req *dialect.Request, err error) *dialect.Response {
	resp := recordRetrieveErrorResponse(req, err)
	if resp != nil && resp.Status >= http.StatusInternalServerError {
		return dashboardInternalErrorResponse()
	}
	return resp
}

func dashboardInternalErrorResponse() *dialect.Response {
	return internalErrorResponse(dashboardGenericInternalErrorMessage)
}

func dashboardResponseFromRecord(record contract.Record) dashboardResponse {
	name := dashboardRecordString(record, "Name")
	return dashboardResponse{
		ID:            dashboardRecordString(record, "Id"),
		Name:          name,
		Label:         name,
		DeveloperName: dashboardRecordString(record, "DeveloperName"),
		FolderID:      dashboardRecordString(record, "FolderId"),
		Description:   dashboardRecordString(record, "Description"),
		LayoutType:    dashboardRecordString(record, "LayoutType"),
	}
}

func dashboardRecordString(record contract.Record, key string) string {
	if record == nil {
		return ""
	}
	if value, ok := record[key].(string); ok {
		return value
	}
	return ""
}

func sanitizeDashboardDeveloperName(name string) string {
	if name == "" {
		return ""
	}
	clean := strings.ReplaceAll(name, " ", "_")
	clean = dashboardNonDeveloperNameChars.ReplaceAllString(clean, "")
	clean = dashboardRepeatedUnderscores.ReplaceAllString(clean, "_")
	clean = strings.Trim(clean, "_")
	if len(clean) > 0 && clean[0] >= '0' && clean[0] <= '9' {
		clean = "X" + clean
	}
	if clean == "" {
		clean = "Unnamed"
	}
	if len(clean) > dashboardDeveloperNameMaxBytes {
		clean = strings.TrimSuffix(clean[:dashboardDeveloperNameMaxBytes], "_")
	}
	return clean
}

func (c *dashboardResultCache) store(dashboardID string, result *dashboard.Result) {
	now := time.Now()
	c.startCleanup()
	c.Lock()
	defer c.Unlock()
	c.cleanupLocked(now)
	c.entries[dashboardID] = &cachedDashboardResult{result: result, timestamp: now}
}

func (c *dashboardResultCache) get(dashboardID string) (*cachedDashboardResult, string) {
	now := time.Now()
	c.RLock()
	cached := c.entries[dashboardID]
	c.RUnlock()
	if cached == nil {
		return nil, "NotFound"
	}
	if now.Sub(cached.timestamp) > dashboardExecutionCacheTTL {
		c.Lock()
		delete(c.entries, dashboardID)
		c.Unlock()
		return nil, "Expired"
	}
	return cached, "Success"
}

func (c *dashboardResultCache) cleanupLocked(now time.Time) {
	for dashboardID, cached := range c.entries {
		if cached == nil || now.Sub(cached.timestamp) > dashboardExecutionCacheTTL {
			delete(c.entries, dashboardID)
		}
	}
}

func (c *dashboardResultCache) startCleanup() {
	c.cleanupOnce.Do(func() {
		go func() {
			ticker := time.NewTicker(dashboardExecutionCacheCleanupInterval)
			defer ticker.Stop()
			for now := range ticker.C {
				c.sweepExpired(now)
			}
		}()
	})
}

func (c *dashboardResultCache) sweepExpired(now time.Time) {
	c.Lock()
	defer c.Unlock()
	c.cleanupLocked(now)
}

func dashboardStatusErrorMessage(status string) string {
	if status == "Expired" {
		return "Cached dashboard result has expired"
	}
	return "Dashboard execution result not found in cache"
}
