package files

import (
	"context"
	"database/sql"
	"fmt"
	"html"
	"regexp"
	"strings"
	"sync"

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

// HrefRewriteField names a (objectApiName, fieldApiName) pair whose value can
// store a legacy Salesforce ContentDistribution share URL (an
// `<a href="https://<sf>/sfc/p/<orgNs>/a/<distId>/<secret>">label</a>`
// snippet). The resolver inspects only registered fields and only on records
// of the matching parent object.
//
// Registration is the authoring surface for which (object, field) pairs carry
// href-rewriteable values. Platform code (this package) declares ZERO pairs
// by default — a greenfield org has no href-bearing columns and the resolver
// is a no-op. Tenant-specific pairs are registered from the tenant-quarantine
// directory (backend/internal/controlplane/tenants/<tenant-id>/) via
// RegisterHrefFields at package init.
type HrefRewriteField struct {
	ParentApiName string
	FieldApiName  string
}

// hrefRewriteRegistry holds the set of (object, field) pairs the resolver
// acts on. It is populated via RegisterHrefFields from the tenant-quarantine
// directory; L0 platform code never adds to it directly. Protected by a
// mutex so init-order race detectors stay quiet, though registration is
// expected to happen during package init (single goroutine).
var (
	hrefRewriteMu       sync.RWMutex
	hrefRewriteRegistry []HrefRewriteField
)

// RegisterHrefFields appends (object, field) pairs to the resolver registry.
// Called from tenant-quarantine package init() so the resolver operates on
// tenant-specific columns without the platform binary containing any tenant
// field literals. Duplicate (object, field) pairs are silently deduped so
// re-registration at test-setup time is safe.
//
// Adding a column here MUST be paired with a frontend renderer override for
// the same field, otherwise the rewritten link becomes invisible to users
// who view the column through the default URL renderer.
func RegisterHrefFields(fields ...HrefRewriteField) {
	hrefRewriteMu.Lock()
	defer hrefRewriteMu.Unlock()
	seen := make(map[HrefRewriteField]struct{}, len(hrefRewriteRegistry)+len(fields))
	for _, f := range hrefRewriteRegistry {
		seen[f] = struct{}{}
	}
	for _, f := range fields {
		if _, dup := seen[f]; dup {
			continue
		}
		seen[f] = struct{}{}
		hrefRewriteRegistry = append(hrefRewriteRegistry, f)
	}
}

// ResetHrefFieldsForTest clears the registry. Test-only helper — production
// code never calls this. Exported under a _test suffix in the name so a
// future linter can flag production-side callers.
func ResetHrefFieldsForTest() {
	hrefRewriteMu.Lock()
	hrefRewriteRegistry = nil
	hrefRewriteMu.Unlock()
}

// snapshotHrefFields returns a read-only copy of the registry. Callers
// iterate the snapshot so a concurrent Register cannot mutate the slice
// mid-iteration.
func snapshotHrefFields() []HrefRewriteField {
	hrefRewriteMu.RLock()
	defer hrefRewriteMu.RUnlock()
	if len(hrefRewriteRegistry) == 0 {
		return nil
	}
	out := make([]HrefRewriteField, len(hrefRewriteRegistry))
	copy(out, hrefRewriteRegistry)
	return out
}

// nexusFileURLPrefix is the local download URL all rewritten anchors point
// at. The frontend renderer keys off this prefix to decide whether a value
// is "resolved" (clickable file chip) or "unresolved" (warning state).
const nexusFileURLPrefix = "/services/data/v63.0/files/"

// hrefAnchorRE captures the value of the first href= attribute in an HTML
// snippet. The legacy SF columns we operate on always store at most one
// anchor; multi-anchor values are unsupported and will only have their first
// href rewritten (intentional — first anchor is the canonical pointer).
var hrefAnchorRE = regexp.MustCompile(`(?i)href=["']([^"']+)["']`)

// HrefFieldResolver rewrites the SF ContentDistribution hrefs stored in the
// HrefRewriteFields columns to point at the matching `_System_File` row on
// the same parent record. Construct via NewHrefFieldResolver and invoke per
// record via RewriteRecord during the record-fetch path.
type HrefFieldResolver struct {
	db *sql.DB
}

// NewHrefFieldResolver returns a resolver bound to the supplied DB. Returns
// nil when db is nil so callers can rely on a nil resolver being a no-op.
func NewHrefFieldResolver(db *sql.DB) *HrefFieldResolver {
	if db == nil {
		return nil
	}
	return &HrefFieldResolver{db: db}
}

// FieldsToRewriteForObject returns the field API names this resolver acts on
// for the given parent object, or nil when the object has no registered
// fields. Useful when a caller's record shape isn't `models.SObject` and the
// caller iterates its own field map (e.g. UI-API handler).
func FieldsToRewriteForObject(objectApiName string) []string {
	var fields []string
	for _, f := range snapshotHrefFields() {
		if strings.EqualFold(f.ParentApiName, objectApiName) {
			fields = append(fields, f.FieldApiName)
		}
	}
	return fields
}

// RewriteValue resolves a single field value: if it parses to an SF
// ContentDistribution href that matches a `_System_File` on the supplied
// parent, returns the rewritten anchor pointing at the local download URL;
// otherwise returns the input unchanged. The boolean indicates whether the
// value was rewritten — useful for skipping no-op writebacks.
//
// Callers using `models.SObject` should prefer RewriteRecord; this method is
// for callers iterating non-SObject record shapes (e.g. the UI API
// `map[string]FieldValue` envelope).
func (r *HrefFieldResolver) RewriteValue(ctx context.Context, parentID string, value string) (string, bool, error) {
	if r == nil || r.db == nil {
		return value, false, nil
	}
	rewritten, err := r.rewriteFieldValue(ctx, parentID, value)
	if err != nil {
		return value, false, err
	}
	return rewritten, rewritten != value, nil
}

// RewriteRecord mutates record's href-bearing fields in place. Idempotent:
// values already pointing at /services/data/v63.0/files/... are left alone.
// Unresolved values (no matching `_System_File` on this record) are also left
// alone so the frontend can detect and render a warning state.
//
// The resolver is best-effort: any query failure is returned for the caller
// to log, but record may have been partially mutated. Callers should still
// serve the (possibly partial) record.
func (r *HrefFieldResolver) RewriteRecord(ctx context.Context, objectApiName string, recordID string, record models.SObject) error {
	if r == nil || r.db == nil || record == nil {
		return nil
	}
	for _, f := range snapshotHrefFields() {
		if !strings.EqualFold(f.ParentApiName, objectApiName) {
			continue
		}
		raw, ok := record[f.FieldApiName].(string)
		if !ok || strings.TrimSpace(raw) == "" {
			continue
		}
		rewritten, err := r.rewriteFieldValue(ctx, recordID, raw)
		if err != nil {
			return fmt.Errorf("href rewrite %s.%s: %w",
				f.ParentApiName, f.FieldApiName, err)
		}
		if rewritten != raw {
			record[f.FieldApiName] = rewritten
		}
	}
	return nil
}

// rewriteFieldValue rewrites a single column's value, handling two shapes:
//
//   1. HTML anchor `<a href="…sfc/p/…">label</a>`. The href is replaced
//      inline and the `label` text (visible to users) is preserved.
//
//   2. Bare URL `https://…sfc/p/…`. The URL is wrapped in an anchor with
//      the resolved file's Name as its label, so the frontend renders a
//      named chip rather than a URL-as-label fallback.
//
// Already-local hrefs and unresolved hrefs are returned untouched. The
// frontend renderer keys off the `/services/data/v63.0/files/...` prefix to
// distinguish resolved (clickable chip) from unresolved (warning chip).
func (r *HrefFieldResolver) rewriteFieldValue(ctx context.Context, parentID, value string) (string, error) {
	if idx := hrefAnchorRE.FindStringSubmatchIndex(value); idx != nil {
		hrefStart, hrefEnd := idx[2], idx[3]
		href := html.UnescapeString(value[hrefStart:hrefEnd])
		if strings.HasPrefix(href, nexusFileURLPrefix) {
			return value, nil
		}
		distIDBody, ok := extractDistributionIDBody(href)
		if !ok {
			return value, nil
		}
		row, err := r.lookupSystemFileByDistribution(ctx, parentID, distIDBody)
		if err != nil || row == nil {
			return value, err
		}
		newHref := nexusFileURLPrefix + row.id + "/download"
		return value[:hrefStart] + newHref + value[hrefEnd:], nil
	}

	// Bare URL path: when the column stores the SF URL alone (no anchor), wrap
	// the rewritten URL in an anchor whose label is the resolved file name so
	// the frontend chip shows a meaningful caption.
	trimmed := strings.TrimSpace(value)
	if !strings.HasPrefix(trimmed, "http://") && !strings.HasPrefix(trimmed, "https://") {
		return value, nil
	}
	if strings.HasPrefix(trimmed, nexusFileURLPrefix) {
		return value, nil
	}
	distIDBody, ok := extractDistributionIDBody(trimmed)
	if !ok {
		return value, nil
	}
	row, err := r.lookupSystemFileByDistribution(ctx, parentID, distIDBody)
	if err != nil || row == nil {
		return value, err
	}
	label := html.EscapeString(row.name)
	return fmt.Sprintf(`<a href="%s%s/download" target="_blank">%s</a>`, nexusFileURLPrefix, row.id, label), nil
}

// extractDistributionIDBody pulls the 12-char ContentDistribution body that
// SF embeds in `/sfc/p/<orgNs>/a/<distIdBody>/<secret>`. Returns false when
// the URL does not match the SF ContentDistribution share-URL shape (a hard
// requirement: the body must be exactly 12 chars between `/a/` and the next
// path separator).
func extractDistributionIDBody(href string) (string, bool) {
	const aSegment = "/a/"
	idx := strings.Index(href, aSegment)
	if idx < 0 {
		return "", false
	}
	rest := href[idx+len(aSegment):]
	end := strings.IndexByte(rest, '/')
	if end != 12 {
		return "", false
	}
	return rest[:end], true
}

// resolvedSystemFile is the minimum shape rewriteFieldValue needs from the
// matched `_System_File` row: the Id for the local URL and the Name for the
// label when wrapping a bare URL.
type resolvedSystemFile struct {
	id   string
	name string
}

// lookupSystemFileByDistribution resolves a 12-char ContentDistribution body
// back to a `_System_File` whose `SourceContentVersionId` matches and whose
// `ParentId` is the same record we are loading. Returns nil with nil error
// when the href has no resolution on this record (the common case for
// records whose archived file lives on a different SF pod or was purged).
func (r *HrefFieldResolver) lookupSystemFileByDistribution(ctx context.Context, parentID, distIDBody string) (*resolvedSystemFile, error) {
	const q = "SELECT sf.`Id`, sf.`Name` FROM `_System_File` sf " +
		"JOIN `ContentDistribution` cd " +
		"  ON cd.`ContentVersionId` = sf.`SourceContentVersionId` " +
		" AND cd.`IsDeleted` = 0 " +
		"WHERE SUBSTRING(cd.`Id`, 4, 12) = ? " +
		"  AND sf.`ParentId` = ? " +
		"  AND (sf.`Status` IS NULL OR sf.`Status` = 'Active') " +
		"ORDER BY sf.`CreatedDate` DESC " +
		"LIMIT 1"
	row := &resolvedSystemFile{}
	var name sql.NullString
	err := r.db.QueryRowContext(ctx, q, distIDBody, parentID).Scan(&row.id, &name)
	if err == sql.ErrNoRows {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("query: %w", err)
	}
	if name.Valid {
		row.name = strings.TrimSpace(name.String)
	}
	if row.name == "" {
		row.name = row.id
	}
	return row, nil
}
