package sfcrmappservice

import (
	"context"
	"errors"
	"fmt"
	"html"
	"net/http"
	"net/url"
	"regexp"
	"strings"

	crmwire "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/wire"
	attachmentsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordattachmentsv1"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordviewv1"
)

type RuntimeRecordViewReadFunc func(context.Context, string, semantic.ReadRequest) (semantic.ReadResponse, error)
type RuntimeAttachmentSourceProofResolveFunc func(context.Context, string, attachmentsemantic.ResolveRequest) (attachmentsemantic.ResolveResponse, error)

type recordViewRoute struct {
	version, objectID, recordID, fieldID, fieldValue string
}

func (route recordViewRoute) externalFieldLookup() bool { return route.fieldID != "" }

var recordViewVersions = map[string]struct{}{
	"v59.0": {}, "v60.0": {}, "v61.0": {}, "v62.0": {}, "v63.0": {},
}

// recordViewDedicatedIDReadObjects is the CRM-owned collision set for the
// generic GET /sobjects/:object/:id shape. Each entry has a dedicated handler
// in the CRM dialect and therefore cannot be sent to the Runtime record-view
// capability. Keep this deliberately local to the L1 route owner rather than
// teaching the generic Runtime about CRM route names.
var recordViewDedicatedIDReadObjects = map[string]struct{}{
	"FlowInterview": {},
}

// matchRecordViewRoute owns only the two CRM read paths selected by D2A. It
// rejects literal siblings that the legacy router resolves before parameters.
func matchRecordViewRoute(path string) (recordViewRoute, bool) {
	escaped := strings.Trim(strings.TrimSpace(path), "/")
	parts := strings.Split(escaped, "/")
	if len(parts) != 6 && len(parts) != 7 {
		return recordViewRoute{}, false
	}
	if parts[0] != "services" || parts[1] != "data" || parts[3] != "sobjects" {
		return recordViewRoute{}, false
	}
	if _, ok := recordViewVersions[parts[2]]; !ok {
		return recordViewRoute{}, false
	}
	decoded := make([]string, len(parts))
	for index, part := range parts {
		value, err := url.PathUnescape(part)
		if err != nil || value == "" {
			return recordViewRoute{}, false
		}
		decoded[index] = value
	}
	route := recordViewRoute{version: decoded[2], objectID: decoded[4]}
	if len(decoded) == 6 {
		if decoded[5] == "describe" || decoded[5] == "listviews" || decoded[5] == "quickActions" {
			return recordViewRoute{}, false
		}
		if _, dedicated := recordViewDedicatedIDReadObjects[decoded[4]]; dedicated {
			return recordViewRoute{}, false
		}
		route.recordID = decoded[5]
		return route, true
	}
	if decoded[5] == "describe" || decoded[5] == "listviews" ||
		(decoded[4] == "_System_File" && decoded[6] == "download") ||
		(decoded[4] == "ContentVersion" && decoded[6] == "VersionData") {
		return recordViewRoute{}, false
	}
	route.fieldID, route.fieldValue = decoded[5], decoded[6]
	return route, true
}

func readCRMRecordView(
	ctx context.Context,
	runtime *RuntimeRecordViewClient,
	bearer string,
	route recordViewRoute,
) (map[string]any, error) {
	request := semantic.ReadRequest{ObjectID: route.objectID, Projection: semantic.ProjectionEnrichedView}
	request.Selector = semantic.Selector{Kind: semantic.SelectorRecordID, RecordID: route.recordID}
	if route.externalFieldLookup() {
		request.Projection = semantic.ProjectionRoot
		request.Selector = semantic.Selector{Kind: semantic.SelectorFieldEqual, FieldID: route.fieldID, FieldValue: route.fieldValue}
	}
	response, err := runtime.Read(ctx, bearer, request)
	if err != nil {
		return nil, err
	}
	if !route.externalFieldLookup() {
		decorateRecordViewReadAttachments(ctx, runtime, bearer, route, &response)
	}
	record, err := recordViewRoot(response.Root)
	if err != nil {
		return nil, err
	}
	for _, display := range response.ReferenceDisplays {
		record[display.RelationshipID] = map[string]any{
			"attributes": map[string]any{"type": display.TargetObjectID},
			"Id":         display.TargetRecordID, "Name": display.DisplayName,
		}
	}
	record["Id"] = response.RecordID
	record["attributes"] = map[string]any{
		"type": route.objectID,
		"url":  fmt.Sprintf("/services/data/%s/sobjects/%s/%s", route.version, route.objectID, response.RecordID),
	}
	return record, nil
}

func recordViewRoot(root semantic.Record) (map[string]any, error) {
	record := make(map[string]any, len(root.Fields)+2)
	for _, field := range root.Fields {
		value, err := recordViewValue(field.Value)
		if err != nil {
			return nil, err
		}
		record[field.Name] = value
	}
	return record, nil
}

func recordViewValue(value semantic.Value) (any, error) {
	if value.Null {
		return nil, nil
	}
	switch value.Kind {
	case semantic.ValueString:
		return dereference(value.String)
	case semantic.ValueBoolean:
		return dereference(value.Boolean)
	case semantic.ValueInteger:
		return dereference(value.Integer)
	case semantic.ValueUnsigned:
		return dereference(value.Unsigned)
	case semantic.ValueDouble:
		return dereference(value.Double)
	case semantic.ValueDecimal:
		if value.Decimal != nil {
			return exactJSONDecimal(decimalText(value.Decimal.Coefficient, value.Decimal.Scale)), nil
		}
	case semantic.ValueDate:
		return dereference(value.Date)
	case semantic.ValueDateTime:
		return dereference(value.DateTime)
	case semantic.ValueReference:
		return dereference(value.Reference)
	case semantic.ValueObject:
		if value.Object != nil {
			mapped, err := recordViewRoot(*value.Object)
			if err != nil {
				return nil, err
			}
			if value.Object.ObjectID != "" {
				mapped["attributes"] = map[string]any{"type": value.Object.ObjectID}
			}
			return mapped, nil
		}
	case semantic.ValueList:
		if value.List != nil {
			mapped := make([]any, 0, len(*value.List))
			for _, item := range *value.List {
				projected, err := recordViewValue(item)
				if err != nil {
					return nil, err
				}
				mapped = append(mapped, projected)
			}
			return mapped, nil
		}
	}
	return nil, &semantic.Failure{Category: semantic.FailureInternal}
}

type parsedAttachmentSource struct {
	fieldID, resolverID, sourceValue string
	sourceValueOffset                uint32
}

var recordViewHrefAnchorRE = regexp.MustCompile(`(?i)href=["']([^"']+)["']`)

// decorateRecordViewReadAttachments applies the one L1-owned attachment
// disclosure projection directly to the neutral read result. Every public
// record representation, including record-ui, must pass through this boundary
// instead of accidentally returning unresolved provider/storage identities.
func decorateRecordViewReadAttachments(
	ctx context.Context,
	runtime *RuntimeRecordViewClient,
	bearer string,
	route recordViewRoute,
	read *semantic.ReadResponse,
) {
	if read == nil || len(read.AttachmentSourceFields) == 0 {
		return
	}
	record, err := recordViewRoot(read.Root)
	if err != nil {
		return
	}
	decorateRecordViewAttachments(ctx, runtime, bearer, route, *read, record)
	bound := make(map[string]struct{}, len(read.AttachmentSourceFields))
	for _, binding := range read.AttachmentSourceFields {
		bound[binding.SourceFieldID] = struct{}{}
	}
	for index := range read.Root.Fields {
		field := &read.Root.Fields[index]
		if _, ok := bound[field.Name]; !ok {
			continue
		}
		value, ok := record[field.Name].(string)
		if !ok || field.Value.Kind != semantic.ValueString || field.Value.String == nil {
			continue
		}
		field.Value.String = &value
	}
}

func decorateRecordViewAttachments(
	ctx context.Context,
	runtime *RuntimeRecordViewClient,
	bearer string,
	route recordViewRoute,
	read semantic.ReadResponse,
	record map[string]any,
) {
	parsed := make([]parsedAttachmentSource, 0, len(read.AttachmentSourceFields))
	rawByField := make(map[string]string, len(read.AttachmentSourceFields))
	for _, binding := range read.AttachmentSourceFields {
		raw, ok := record[binding.SourceFieldID].(string)
		if !ok || strings.TrimSpace(raw) == "" {
			continue
		}
		// AttachmentSourceFields marks provider/storage identities which are not
		// public CRM values. Default-deny before parsing or Runtime resolution so
		// every malformed, unavailable, canceled, or mixed-resolver path remains
		// safe. Only an authorized resolved result may replace this value.
		rawByField[binding.SourceFieldID] = raw
		record[binding.SourceFieldID] = "File unavailable"
	}

	resolverID := ""
	for _, binding := range read.AttachmentSourceFields {
		raw, ok := rawByField[binding.SourceFieldID]
		if !ok {
			continue
		}
		source, ok := parseRecordAttachmentSource(binding.SourceFieldID, binding.ResolverID, raw)
		if !ok {
			continue
		}
		if resolverID != "" && resolverID != binding.ResolverID {
			// The capability permits one optional batched follow-up. Resolver
			// diversity therefore remains redacted without creating fan-out.
			return
		}
		resolverID = binding.ResolverID
		parsed = append(parsed, source)
	}
	if len(parsed) == 0 {
		return
	}
	request := attachmentsemantic.ResolveRequest{
		ParentObjectID: route.objectID, ParentRecordID: read.RecordID, ResolverID: resolverID,
		Sources: make([]attachmentsemantic.Source, len(parsed)),
	}
	if read.Receipts.CatalogRevision != 0 {
		revision := read.Receipts.CatalogRevision
		request.MinCatalogRevision = &revision
	}
	for index := range parsed {
		request.Sources[index] = attachmentsemantic.Source{
			SourceFieldID: parsed[index].fieldID, SourceValue: parsed[index].sourceValue, SourceValueOffset: parsed[index].sourceValueOffset,
		}
	}
	response, err := runtime.ResolveAttachmentSourceProofs(ctx, bearer, request)
	if err != nil {
		return
	}
	for _, result := range response.Results {
		// ValidateResolveResponse binds every result to one request index before
		// this product projection runs. Retain the protocol index here rather
		// than treating response position as an implicit correlation rule.
		if result.Index >= uint32(len(parsed)) {
			return
		}
		source := parsed[result.Index]
		switch result.Outcome {
		case attachmentsemantic.SourceNotFound:
			continue
		case attachmentsemantic.SourceUnavailable:
			continue
		case attachmentsemantic.SourceResolved:
			download := fmt.Sprintf("/services/data/%s/files/%s/download", route.version, result.FileID)
			record[source.fieldID] = fmt.Sprintf(`<a href="%s" target="_blank" rel="noopener noreferrer">%s</a>`, download, html.EscapeString(result.FileName))
		}
	}
}

func parseRecordAttachmentSource(fieldID, resolverID, value string) (parsedAttachmentSource, bool) {
	if match := recordViewHrefAnchorRE.FindStringSubmatchIndex(value); match != nil {
		body, bodyOffset, ok := distributionIDBody(value[match[2]:match[3]])
		if !ok || uint64(match[2])+uint64(bodyOffset) > uint64(^uint32(0)) {
			return parsedAttachmentSource{}, false
		}
		return parsedAttachmentSource{
			fieldID: fieldID, resolverID: resolverID, sourceValue: body, sourceValueOffset: uint32(match[2] + bodyOffset),
		}, true
	}
	trimmed := strings.TrimSpace(value)
	if !strings.HasPrefix(trimmed, "http://") && !strings.HasPrefix(trimmed, "https://") {
		return parsedAttachmentSource{}, false
	}
	body, bodyOffset, ok := distributionIDBody(trimmed)
	trimmedOffset := strings.Index(value, trimmed)
	if !ok || trimmedOffset < 0 || uint64(trimmedOffset)+uint64(bodyOffset) > uint64(^uint32(0)) {
		return parsedAttachmentSource{}, false
	}
	return parsedAttachmentSource{
		fieldID: fieldID, resolverID: resolverID, sourceValue: body, sourceValueOffset: uint32(trimmedOffset + bodyOffset),
	}, true
}

// distributionIDBody is CRM-owned URL grammar. It returns the exact raw byte
// offset of the parsed source body inside href, rather than a rune index or an
// offset in an HTML-unescaped copy. Runtime independently proves those raw
// parent-field bytes before it can use the Catalog-declared source matcher.
func distributionIDBody(href string) (string, int, bool) {
	trimmed := strings.TrimSpace(href)
	trimmedOffset := strings.Index(href, trimmed)
	parsed, err := url.Parse(trimmed)
	if err != nil {
		return "", 0, false
	}
	escapedPath := parsed.EscapedPath()
	pathOffset := strings.Index(trimmed, escapedPath)
	if trimmedOffset < 0 || escapedPath == "" || pathOffset < 0 {
		return "", 0, false
	}
	type segment struct {
		value  string
		offset int
	}
	segments := make([]segment, 0, 8)
	for index := 0; index < len(escapedPath); {
		for index < len(escapedPath) && escapedPath[index] == '/' {
			index++
		}
		start := index
		for index < len(escapedPath) && escapedPath[index] != '/' {
			index++
		}
		if start < index {
			segments = append(segments, segment{value: escapedPath[start:index], offset: start})
		}
	}
	for index := 0; index+5 < len(segments); index++ {
		if segments[index].value != "sfc" || segments[index+1].value != "p" || segments[index+3].value != "a" {
			continue
		}
		body := segments[index+4]
		if len(body.value) != 12 || segments[index+5].value == "" {
			return "", 0, false
		}
		return body.value, trimmedOffset + pathOffset + body.offset, true
	}
	return "", 0, false
}

func writeRecordViewFailure(writer http.ResponseWriter, route recordViewRoute, err error) {
	var failure *semantic.Failure
	if errors.As(err, &failure) {
		if (failure.Category == semantic.FailureInvalidRequest && failure.InvalidComponent == semantic.InvalidComponentObjectID) ||
			(failure.Category == semantic.FailureResourceExhausted && failure.LimitKind == semantic.LimitObjectID) {
			writeCRMErrorWith(writer, http.StatusNotFound, crmwire.SFCodeNotFound, "Object not found", writeLegacyJSON)
			return
		}
		if route.externalFieldLookup() &&
			((failure.Category == semantic.FailureInvalidRequest && failure.InvalidComponent == semantic.InvalidComponentField) ||
				(failure.Category == semantic.FailureResourceExhausted && failure.LimitKind == semantic.LimitFieldID)) {
			writeCRMErrorWith(writer, http.StatusBadRequest, crmwire.SFCodeInvalidField, fmt.Sprintf("Field '%s' does not exist on object '%s'", route.fieldID, route.objectID), writeLegacyJSON)
			return
		}
		if !route.externalFieldLookup() && failure.Category == semantic.FailureResourceExhausted && failure.LimitKind == semantic.LimitRecordID {
			writeCRMErrorWith(writer, http.StatusNotFound, crmwire.SFCodeNotFound, "Record not found", writeLegacyJSON)
			return
		}
	}
	writeRuntimeFailureWith(writer, err, writeLegacyJSON)
}
