package handler

import (
	"fmt"
	"net"
	"strings"
	"time"

	coremodel "security-center-backend/internal/model"
	caasmmodel "security-center-backend/internal/modules/caasm/model"
	"security-center-backend/internal/modules/caasm/repository"
	"security-center-backend/internal/modules/caasm/service"
	"security-center-backend/internal/service/directory"
)

type cloudAccountSummary struct {
	ID       uint64                        `json:"id"`
	Name     string                        `json:"name"`
	Provider caasmmodel.CloudProvider      `json:"provider"`
	Status   caasmmodel.CloudAccountStatus `json:"status"`
}

type caasmThrottlingLimitsDTO struct {
	TokenCapacity     int                                         `json:"token_capacity,omitempty"`
	TokenRefillPerSec float64                                     `json:"token_refill_per_sec,omitempty"`
	AdaptiveThreshold int                                         `json:"adaptive_threshold,omitempty"`
	AdaptiveWindowSec int                                         `json:"adaptive_window_sec,omitempty"`
	CoolOffSec        int                                         `json:"cool_off_sec,omitempty"`
	MaxConcurrency    int                                         `json:"max_concurrency,omitempty"`
	MinConcurrency    int                                         `json:"min_concurrency,omitempty"`
	CircuitBreaker    *caasmmodel.CircuitBreakerConfig            `json:"circuit_breaker,omitempty"`
	ServiceLimits     map[string]caasmmodel.ThrottlingServiceSpec `json:"service_limits,omitempty"`
}

type caasmEffectiveThrottlingDTO struct {
	Enabled bool `json:"enabled"`
	caasmThrottlingLimitsDTO
	Sources map[string]string `json:"sources,omitempty"`
}

type caasmThrottlingContractDTO struct {
	RejectOutOfRange  bool                                `json:"reject_out_of_range"`
	OverridePolicy    string                              `json:"override_policy"`
	CoordinationMode  string                              `json:"coordination_mode"`
	HardCaps          caasmThrottlingLimitsDTO            `json:"hard_caps"`
	Defaults          caasmThrottlingLimitsDTO            `json:"defaults"`
	Presets           map[string]caasmThrottlingPresetDTO `json:"presets,omitempty"`
	SupportedServices []string                            `json:"supported_services,omitempty"`
}

type caasmThrottlingPresetDTO struct {
	Label       string                   `json:"label"`
	Description string                   `json:"description,omitempty"`
	Config      caasmThrottlingLimitsDTO `json:"config"`
}

type caasmConfigResponseDTO struct {
	Config              map[string]interface{}      `json:"config"`
	EffectiveThrottling caasmEffectiveThrottlingDTO `json:"effective_throttling"`
	ThrottlingContract  caasmThrottlingContractDTO  `json:"throttling_contract"`
}

type cloudAccountDTO struct {
	ID                  uint64                        `json:"id"`
	Name                string                        `json:"name"`
	Provider            caasmmodel.CloudProvider      `json:"provider"`
	ExternalID          string                        `json:"external_id"`
	Status              caasmmodel.CloudAccountStatus `json:"status"`
	Metadata            map[string]interface{}        `json:"metadata,omitempty"`
	CreatedAt           *time.Time                    `json:"created_at,omitempty"`
	UpdatedAt           *time.Time                    `json:"updated_at,omitempty"`
	LastSyncAt          *time.Time                    `json:"last_sync_at,omitempty"`
	ErrorMessage        string                        `json:"error_message,omitempty"`
	EffectiveThrottling *caasmEffectiveThrottlingDTO  `json:"effective_throttling,omitempty"`
	ThrottlingContract  *caasmThrottlingContractDTO   `json:"throttling_contract,omitempty"`
}

type employeeSummary struct {
	ID           uint64 `json:"id"`
	Name         string `json:"name"`
	Email        string `json:"email"`
	AvatarURL    string `json:"avatar_url,omitempty"`
	DepartmentID string `json:"department_id,omitempty"`
}

type departmentSummary struct {
	ID               uint64 `json:"id"`
	LarkDepartmentID string `json:"lark_department_id"`
	Name             string `json:"name"`
}

type caasmAssetListItemDTO struct {
	ID              uint64                 `json:"id"`
	Name            string                 `json:"name"`
	NativeID        string                 `json:"native_id,omitempty"`
	AssetType       caasmmodel.AssetType   `json:"asset_type"`
	Service         string                 `json:"service,omitempty"`
	Criticality     string                 `json:"criticality,omitempty"`
	Status          caasmmodel.AssetStatus `json:"status"`
	OwnerEmployeeID *uint64                `json:"owner_employee_id,omitempty"`
	DepartmentID    *string                `json:"department_id,omitempty"`
	OwnerEmployee   *employeeSummary       `json:"owner_employee,omitempty"`
	Department      *departmentSummary     `json:"department,omitempty"`
	Environment     string                 `json:"environment,omitempty"`
	Region          string                 `json:"region,omitempty"`
	Provider        string                 `json:"provider,omitempty"`
	CloudAccountID  *uint64                `json:"cloud_account_id,omitempty"`
	CloudAccount    *cloudAccountSummary   `json:"cloud_account,omitempty"`
	IPAddresses     []string               `json:"ip_addresses,omitempty"`
	LastSeenAt      *time.Time             `json:"last_seen_at,omitempty"`
}

type caasmAPIRouteConfigRequest struct {
	Name    string `json:"name,omitempty"`
	RawYAML string `json:"raw_yaml" binding:"required"`
}

type caasmAPIRouteConfigSummaryDTO struct {
	ID          uint64     `json:"id"`
	Name        string     `json:"name"`
	SourceType  string     `json:"source_type"`
	YAMLSHA256  string     `json:"yaml_sha256"`
	Status      string     `json:"status"`
	RouteCount  int        `json:"route_count"`
	UploadedBy  *uint64    `json:"uploaded_by,omitempty"`
	IsActive    bool       `json:"is_active"`
	ActivatedAt *time.Time `json:"activated_at,omitempty"`
	CreatedAt   time.Time  `json:"created_at"`
	UpdatedAt   time.Time  `json:"updated_at"`
}

type caasmAPIRouteAssetDTO struct {
	ID                uint64                         `json:"id"`
	ConfigID          uint64                         `json:"config_id"`
	GroupIndex        int                            `json:"group_index"`
	IngressIndex      int                            `json:"ingress_index"`
	ServiceIndex      int                            `json:"service_index"`
	RouteIndex        int                            `json:"route_index"`
	RouteKind         string                         `json:"route_kind"`
	RoutePath         string                         `json:"route_path"`
	RouteKey          string                         `json:"route_key"`
	Domains           interface{}                    `json:"domains"`
	NamespaceMap      interface{}                    `json:"namespace_map"`
	IngressNamePrefix string                         `json:"ingress_name_prefix"`
	ServiceName       string                         `json:"service_name"`
	ServicePort       *int                           `json:"service_port,omitempty"`
	External          bool                           `json:"external"`
	StripPath         bool                           `json:"strip_path"`
	Plugins           []string                       `json:"plugins"`
	RawPlugins        string                         `json:"raw_plugins,omitempty"`
	OccurrenceCount   int                            `json:"occurrence_count"`
	CreatedAt         time.Time                      `json:"created_at"`
	UpdatedAt         time.Time                      `json:"updated_at"`
	DeletedAt         *time.Time                     `json:"deleted_at,omitempty"`
	Config            *caasmAPIRouteConfigSummaryDTO `json:"config,omitempty"`
}

type caasmAPIRoutePreviewDTO struct {
	Name       string                  `json:"name"`
	YAMLSHA256 string                  `json:"yaml_sha256"`
	RouteCount int                     `json:"route_count"`
	Preview    []caasmAPIRouteAssetDTO `json:"preview"`
	Error      string                  `json:"error,omitempty"`
}

func toCAASMAPIRouteConfigSummaryDTO(summary *service.APIRouteConfigSummary) *caasmAPIRouteConfigSummaryDTO {
	if summary == nil {
		return nil
	}
	return &caasmAPIRouteConfigSummaryDTO{
		ID:          summary.ID,
		Name:        summary.Name,
		SourceType:  summary.SourceType,
		YAMLSHA256:  summary.YAMLSHA256,
		Status:      summary.Status,
		RouteCount:  summary.RouteCount,
		UploadedBy:  summary.UploadedBy,
		IsActive:    summary.IsActive,
		ActivatedAt: summary.ActivatedAt,
		CreatedAt:   summary.CreatedAt,
		UpdatedAt:   summary.UpdatedAt,
	}
}

func toCAASMAPIRouteConfigSummaryDTOFromModel(config caasmmodel.APIRouteConfig) *caasmAPIRouteConfigSummaryDTO {
	if config.ID == 0 {
		return nil
	}
	return &caasmAPIRouteConfigSummaryDTO{
		ID:          config.ID,
		Name:        config.Name,
		SourceType:  config.SourceType,
		YAMLSHA256:  config.YAMLSHA256,
		Status:      config.Status,
		RouteCount:  config.RouteCount,
		UploadedBy:  config.UploadedBy,
		IsActive:    config.IsActive,
		ActivatedAt: config.ActivatedAt,
		CreatedAt:   config.CreatedAt,
		UpdatedAt:   config.UpdatedAt,
	}
}

func toCAASMAPIRouteAssetDTO(asset caasmmodel.APIRouteAsset) caasmAPIRouteAssetDTO {
	return caasmAPIRouteAssetDTO{
		ID:                asset.ID,
		ConfigID:          asset.ConfigID,
		GroupIndex:        asset.GroupIndex,
		IngressIndex:      asset.IngressIndex,
		ServiceIndex:      asset.ServiceIndex,
		RouteIndex:        asset.RouteIndex,
		RouteKind:         asset.RouteKind,
		RoutePath:         asset.RoutePath,
		RouteKey:          asset.RouteKey,
		Domains:           asset.Domains,
		NamespaceMap:      asset.NamespaceMap,
		IngressNamePrefix: asset.IngressNamePrefix,
		ServiceName:       asset.ServiceName,
		ServicePort:       asset.ServicePort,
		External:          asset.External,
		StripPath:         asset.StripPath,
		Plugins:           []string(asset.Plugins),
		RawPlugins:        asset.RawPlugins,
		OccurrenceCount:   asset.OccurrenceCount,
		CreatedAt:         asset.CreatedAt,
		UpdatedAt:         asset.UpdatedAt,
		DeletedAt:         asset.DeletedAt,
		Config:            toCAASMAPIRouteConfigSummaryDTOFromModel(asset.Config),
	}
}

func toCAASMAPIRouteAssetDTOs(items []caasmmodel.APIRouteAsset) []caasmAPIRouteAssetDTO {
	out := make([]caasmAPIRouteAssetDTO, 0, len(items))
	for _, item := range items {
		out = append(out, toCAASMAPIRouteAssetDTO(item))
	}
	return out
}

func toCAASMAPIRoutePreviewDTO(preview *service.APIRouteConfigPreview) caasmAPIRoutePreviewDTO {
	if preview == nil {
		return caasmAPIRoutePreviewDTO{}
	}
	return caasmAPIRoutePreviewDTO{
		Name:       preview.Name,
		YAMLSHA256: preview.YAMLSHA256,
		RouteCount: preview.RouteCount,
		Preview:    toCAASMAPIRouteAssetDTOs(preview.Preview),
		Error:      preview.Error,
	}
}

func toCaasmAssetListItemDTO(asset caasmmodel.Asset) caasmAssetListItemDTO {
	var cloudAccount *cloudAccountSummary
	if asset.CloudAccount.ID != 0 {
		cloudAccount = &cloudAccountSummary{
			ID:       asset.CloudAccount.ID,
			Name:     asset.CloudAccount.Name,
			Provider: asset.CloudAccount.Provider,
			Status:   asset.CloudAccount.Status,
		}
	}
	return caasmAssetListItemDTO{
		ID:              asset.ID,
		Name:            asset.Name,
		NativeID:        asset.NativeID,
		AssetType:       asset.AssetType,
		Service:         asset.Service,
		Criticality:     asset.Criticality,
		Status:          asset.Status,
		OwnerEmployeeID: asset.OwnerEmployeeID,
		DepartmentID:    asset.DepartmentID,
		OwnerEmployee:   summarizeEmployee(asset.OwnerEmployee),
		Department:      summarizeDepartment(asset.Department),
		Environment:     asset.Environment,
		Region:          asset.Region,
		Provider:        asset.Provider,
		CloudAccountID:  asset.CloudAccountID,
		CloudAccount:    cloudAccount,
		IPAddresses:     caasmIPAssetAddresses(asset.IPAssets),
		LastSeenAt:      asset.LastSeenAt,
	}
}

func caasmIPAssetAddresses(assets []caasmmodel.IPAsset) []string {
	addresses := make([]string, 0, len(assets))
	seen := make(map[string]struct{}, len(assets))
	for _, asset := range assets {
		address := strings.TrimSpace(asset.IPAddress)
		if address == "" {
			continue
		}
		if _, ok := seen[address]; ok {
			continue
		}
		seen[address] = struct{}{}
		addresses = append(addresses, address)
	}
	return addresses
}

type caasmIPAssetListItemDTO struct {
	ID               uint64                `json:"id"`
	AssetID          uint64                `json:"asset_id"`
	IPAddress        string                `json:"ip_address"`
	Hostname         string                `json:"hostname,omitempty"`
	SourceService    string                `json:"source_service,omitempty"`
	ResourceID       string                `json:"resource_id,omitempty"`
	OwningResource   string                `json:"owning_resource,omitempty"`
	OwningResourceID string                `json:"owning_resource_id,omitempty"`
	OwnershipStatus  string                `json:"ownership_status,omitempty"`
	Exposure         string                `json:"exposure"`
	Freshness        string                `json:"freshness_state"`
	Stale            bool                  `json:"stale"`
	IsStale          bool                  `json:"is_stale"`
	GeoLocation      coremodel.JSONB       `json:"geo_location,omitempty"`
	IsPublic         bool                  `json:"is_public"`
	Asset            caasmAssetListItemDTO `json:"asset"`
	FirstSeenAt      *time.Time            `json:"first_seen_at,omitempty"`
	LastSeenAt       *time.Time            `json:"last_seen_at,omitempty"`
}

func toCaasmIPAssetListItemDTO(entry caasmmodel.IPAsset) caasmIPAssetListItemDTO {
	exposure := "private"
	if entry.IsPublic {
		exposure = "public"
	}
	freshness, stale := caasmIPAssetFreshness(entry)
	return caasmIPAssetListItemDTO{
		ID:               entry.ID,
		AssetID:          entry.AssetID,
		IPAddress:        entry.IPAddress,
		Hostname:         entry.Hostname,
		SourceService:    jsonbString(entry.GeoLocation, "source_service"),
		ResourceID:       jsonbString(entry.GeoLocation, "resource_id"),
		OwningResource:   firstNonEmptyString(jsonbString(entry.GeoLocation, "owning_resource"), entry.Asset.NativeID),
		OwningResourceID: firstNonEmptyString(jsonbString(entry.GeoLocation, "owning_resource_id"), jsonbString(entry.GeoLocation, "resource_id"), entry.Asset.NativeID),
		OwnershipStatus:  jsonbString(entry.GeoLocation, "ownership_status"),
		Exposure:         exposure,
		Freshness:        freshness,
		Stale:            stale,
		IsStale:          stale,
		GeoLocation:      entry.GeoLocation,
		IsPublic:         entry.IsPublic,
		Asset:            toCaasmAssetListItemDTO(entry.Asset),
		FirstSeenAt:      entry.FirstSeenAt,
		LastSeenAt:       entry.LastSeenAt,
	}
}

func caasmIPAssetFreshness(entry caasmmodel.IPAsset) (string, bool) {
	if entry.DeletedAt != nil {
		return "deleted", true
	}
	if entry.LastSeenAt == nil || entry.LastSeenAt.IsZero() {
		return "stale", true
	}
	stale := time.Since(*entry.LastSeenAt) > 24*time.Hour
	if stale {
		return "stale", true
	}
	return "fresh", false
}

func toCaasmThrottlingLimitsDTO(cfg caasmmodel.ThrottlingConfig) caasmThrottlingLimitsDTO {
	return caasmThrottlingLimitsDTO{
		TokenCapacity:     cfg.TokenCapacity,
		TokenRefillPerSec: cfg.TokenRefillPerSec,
		AdaptiveThreshold: cfg.AdaptiveThreshold,
		AdaptiveWindowSec: cfg.AdaptiveWindowSec,
		CoolOffSec:        cfg.CoolOffSec,
		MaxConcurrency:    cfg.MaxConcurrency,
		MinConcurrency:    cfg.MinConcurrency,
		CircuitBreaker:    cfg.CircuitBreaker,
		ServiceLimits:     cfg.ServiceLimits,
	}
}

func toCaasmEffectiveThrottlingDTO(resolution service.ThrottlingResolution) caasmEffectiveThrottlingDTO {
	return caasmEffectiveThrottlingDTO{
		Enabled:                  resolution.Effective.Enabled,
		caasmThrottlingLimitsDTO: toCaasmThrottlingLimitsDTO(resolution.Effective),
		Sources:                  resolution.Sources,
	}
}

func toCaasmThrottlingPresetDTOs() map[string]caasmThrottlingPresetDTO {
	presets := caasmmodel.DefaultThrottlePresets()
	if len(presets) == 0 {
		return nil
	}
	out := make(map[string]caasmThrottlingPresetDTO, len(presets))
	for key, preset := range presets {
		out[key] = caasmThrottlingPresetDTO{
			Label:       preset.Label,
			Description: preset.Description,
			Config:      toCaasmThrottlingLimitsDTO(preset.Config),
		}
	}
	return out
}

func toCaasmThrottlingContractDTO() caasmThrottlingContractDTO {
	defaults := caasmmodel.ConfigDefaults().Throttling
	if defaults == nil {
		defaults = &caasmmodel.ThrottlingConfig{}
	}
	hardCaps := caasmmodel.ThrottlingConfig{
		Enabled:           true,
		TokenCapacity:     caasmmodel.HardCapThrottleTokenCapacity,
		TokenRefillPerSec: float64(caasmmodel.HardCapThrottleTokenRefillPerSec),
		AdaptiveThreshold: caasmmodel.HardCapThrottleAdaptiveThreshold,
		AdaptiveWindowSec: caasmmodel.HardCapThrottleAdaptiveWindowSec,
		CoolOffSec:        caasmmodel.HardCapThrottleCoolOffSec,
		MaxConcurrency:    caasmmodel.HardCapThrottleMaxConcurrency,
		MinConcurrency:    caasmmodel.HardCapThrottleMinConcurrency,
		CircuitBreaker: &caasmmodel.CircuitBreakerConfig{
			Threshold:  caasmmodel.HardCapThrottleAdaptiveThreshold,
			CoolOffSec: caasmmodel.HardCapThrottleCoolOffSec,
		},
	}
	return caasmThrottlingContractDTO{
		RejectOutOfRange:  true,
		OverridePolicy:    "inherit_or_narrow",
		CoordinationMode:  "db_backed",
		HardCaps:          toCaasmThrottlingLimitsDTO(hardCaps),
		Defaults:          toCaasmThrottlingLimitsDTO(*defaults),
		Presets:           toCaasmThrottlingPresetDTOs(),
		SupportedServices: service.SupportedThrottleServiceKeys(),
	}
}

func toCaasmConfigResponseDTO(config map[string]interface{}, resolution service.ThrottlingResolution) caasmConfigResponseDTO {
	return caasmConfigResponseDTO{
		Config:              config,
		EffectiveThrottling: toCaasmEffectiveThrottlingDTO(resolution),
		ThrottlingContract:  toCaasmThrottlingContractDTO(),
	}
}

func toCloudAccountDTO(account *caasmmodel.CloudAccount, resolution *service.ThrottlingResolution) cloudAccountDTO {
	dto := cloudAccountDTO{
		ID:           account.ID,
		Name:         account.Name,
		Provider:     account.Provider,
		ExternalID:   account.ExternalID,
		Status:       account.Status,
		Metadata:     map[string]interface{}(account.Metadata),
		CreatedAt:    &account.CreatedAt,
		UpdatedAt:    &account.UpdatedAt,
		LastSyncAt:   account.LastSyncAt,
		ErrorMessage: account.ErrorMessage,
	}
	if resolution != nil {
		effective := toCaasmEffectiveThrottlingDTO(*resolution)
		dto.EffectiveThrottling = &effective
		contract := toCaasmThrottlingContractDTO()
		dto.ThrottlingContract = &contract
	}
	return dto
}

func summarizeEmployee(emp *directory.Employee) *employeeSummary {
	if emp == nil || emp.ID == 0 {
		return nil
	}
	out := &employeeSummary{
		ID:           emp.ID,
		Name:         emp.Name,
		Email:        emp.Email,
		AvatarURL:    emp.AvatarURL,
		DepartmentID: emp.DepartmentID,
	}
	return out
}

func summarizeDepartment(dept *directory.Department) *departmentSummary {
	if dept == nil || dept.ID == 0 {
		return nil
	}
	return &departmentSummary{
		ID:               dept.ID,
		LarkDepartmentID: dept.LarkDepartmentID,
		Name:             dept.Name,
	}
}

type caasmPortListItemDTO struct {
	ID                uint64      `json:"id"`
	AssetID           uint64      `json:"asset_id"`
	IPAddress         string      `json:"ip_address"`
	Endpoint          string      `json:"endpoint"`
	EndpointType      string      `json:"endpoint_type,omitempty"`
	Evidence          []string    `json:"evidence,omitempty"`
	EvidenceSource    string      `json:"evidence_source,omitempty"`
	EvidenceSources   []string    `json:"evidence_sources,omitempty"`
	SourceService     string      `json:"source_service,omitempty"`
	ResourceID        string      `json:"resource_id,omitempty"`
	OwnershipStatus   string      `json:"ownership_status,omitempty"`
	DNSRecords        interface{} `json:"dns_records,omitempty"`
	ResolutionIPs     []string    `json:"resolution_ips,omitempty"`
	Confidence        string      `json:"confidence,omitempty"`
	Port              uint32      `json:"port"`
	Protocol          string      `json:"protocol"`
	ObservationSource string      `json:"observation_source,omitempty"`
	ServiceName       string      `json:"service_name,omitempty"`
	AssetService      string      `json:"asset_service,omitempty"`
	Product           string      `json:"product,omitempty"`
	Version           string      `json:"version,omitempty"`
	Exposure          string      `json:"exposure,omitempty"`
	Status            string      `json:"status,omitempty"`
	LastSeenAt        time.Time   `json:"last_seen_at"`
}

type caasmPortEvidenceSectionDTO struct {
	Available  bool                   `json:"available"`
	Title      string                 `json:"title"`
	Summary    string                 `json:"summary"`
	Source     string                 `json:"source,omitempty"`
	Confidence string                 `json:"confidence,omitempty"`
	Metadata   map[string]interface{} `json:"metadata,omitempty"`
}

type caasmPortScannerResultDTO struct {
	ID              uint64    `json:"id"`
	BatchID         uint64    `json:"batch_id"`
	RequestID       string    `json:"request_id,omitempty"`
	ScannerBatchID  string    `json:"scanner_batch_id,omitempty"`
	Result          string    `json:"result"`
	ReasonCode      string    `json:"reason_code,omitempty"`
	ConnectMS       int64     `json:"connect_ms"`
	ScannerSourceIP string    `json:"scanner_source_ip,omitempty"`
	ObservedAt      time.Time `json:"observed_at"`
}

type caasmPortEvidenceDTO struct {
	ExposureReason  string                      `json:"exposure_reason"`
	Evidence        []string                    `json:"evidence,omitempty"`
	Confidence      string                      `json:"confidence,omitempty"`
	SourceService   string                      `json:"source_service,omitempty"`
	ResourceID      string                      `json:"resource_id,omitempty"`
	OwnershipStatus string                      `json:"ownership_status,omitempty"`
	DNSRecords      interface{}                 `json:"dns_records,omitempty"`
	ResolutionIPs   []string                    `json:"resolution_ips,omitempty"`
	SecurityGroup   caasmPortEvidenceSectionDTO `json:"security_group"`
	Route           caasmPortEvidenceSectionDTO `json:"route"`
	ScannerResult   *caasmPortScannerResultDTO  `json:"scanner_result,omitempty"`
	Warnings        []string                    `json:"warnings,omitempty"`
}

type caasmPortDetailDTO struct {
	Port     caasmPortListItemDTO   `json:"port"`
	Asset    *caasmAssetListItemDTO `json:"asset,omitempty"`
	Evidence caasmPortEvidenceDTO   `json:"evidence"`
}

func toCaasmPortListItemDTO(entry caasmmodel.IPPortIndex) caasmPortListItemDTO {
	endpoint := entry.EndpointValue()
	literalIP := ""
	if ip := strings.TrimSpace(entry.IPAddress); net.ParseIP(ip) != nil {
		literalIP = ip
	} else if net.ParseIP(endpoint) != nil {
		literalIP = endpoint
	}
	evidence := caasmPortEvidenceKeys(entry, nil)
	return caasmPortListItemDTO{
		ID:                entry.ID,
		AssetID:           entry.AssetID,
		IPAddress:         literalIP,
		Endpoint:          endpoint,
		EndpointType:      caasmPortEndpointType(endpoint),
		Evidence:          evidence,
		EvidenceSource:    caasmPrimaryPortEvidence(evidence),
		EvidenceSources:   evidence,
		SourceService:     jsonbString(entry.Evidence, "source_service"),
		ResourceID:        jsonbString(entry.Evidence, "resource_id"),
		OwnershipStatus:   jsonbString(entry.Evidence, "ownership_status"),
		DNSRecords:        entry.Evidence["dns_records"],
		ResolutionIPs:     jsonbStringSlice(entry.Evidence, "resolution_ips"),
		Confidence:        caasmPortConfidence(evidence),
		Port:              entry.Port,
		Protocol:          entry.Protocol,
		ObservationSource: string(entry.ObservationSource),
		ServiceName:       entry.ServiceName,
		AssetService:      entry.AssetService,
		Product:           entry.Product,
		Version:           entry.Version,
		Exposure:          entry.Exposure,
		Status:            entry.Status,
		LastSeenAt:        entry.LastSeenAt,
	}
}

func toCaasmPortDetailDTO(detail *repository.PortDetail) caasmPortDetailDTO {
	if detail == nil {
		return caasmPortDetailDTO{}
	}
	port := toCaasmPortListItemDTO(detail.Port)
	var asset *caasmAssetListItemDTO
	if detail.Port.Asset.ID != 0 {
		dto := toCaasmAssetListItemDTO(detail.Port.Asset)
		asset = &dto
	}
	evidence := caasmPortEvidenceDTO{
		ExposureReason: caasmPortExposureReason(detail.Port),
		SecurityGroup:  caasmPortSecurityGroupEvidence(detail.Port),
		Route:          caasmPortRouteEvidence(detail.Port),
		Warnings:       caasmPortEvidenceWarnings(detail.Port),
	}
	if detail.LatestScannerObservation != nil {
		obs := detail.LatestScannerObservation
		evidence.ScannerResult = &caasmPortScannerResultDTO{
			ID:              obs.ID,
			BatchID:         obs.BatchID,
			RequestID:       obs.RequestID,
			ScannerBatchID:  stringValue(obs.ScannerBatchID),
			Result:          obs.Result,
			ReasonCode:      obs.ReasonCode,
			ConnectMS:       obs.ConnectMS,
			ScannerSourceIP: stringValue(obs.ScannerSourceIP),
			ObservedAt:      obs.ObservedAt,
		}
	}
	evidence.Evidence = caasmPortEvidenceKeys(detail.Port, detail.LatestScannerObservation)
	evidence.Confidence = caasmPortConfidence(evidence.Evidence)
	evidence.SourceService = jsonbString(detail.Port.Evidence, "source_service")
	evidence.ResourceID = jsonbString(detail.Port.Evidence, "resource_id")
	evidence.OwnershipStatus = jsonbString(detail.Port.Evidence, "ownership_status")
	evidence.DNSRecords = detail.Port.Evidence["dns_records"]
	evidence.ResolutionIPs = jsonbStringSlice(detail.Port.Evidence, "resolution_ips")
	port.Evidence = evidence.Evidence
	port.EvidenceSource = caasmPrimaryPortEvidence(evidence.Evidence)
	port.EvidenceSources = evidence.Evidence
	port.Confidence = evidence.Confidence
	return caasmPortDetailDTO{Port: port, Asset: asset, Evidence: evidence}
}

func caasmPortEndpointType(endpoint string) string {
	endpoint = strings.TrimSpace(endpoint)
	if endpoint == "" {
		return ""
	}
	if net.ParseIP(endpoint) != nil {
		return "ip"
	}
	return "hostname"
}

func caasmPortExposureReason(entry caasmmodel.IPPortIndex) string {
	source := string(entry.ObservationSource)
	switch entry.ObservationSource {
	case caasmmodel.PortObservationSourceSecurityGroup:
		return "Security group rule inferred this endpoint and port as open."
	case caasmmodel.PortObservationSourceListener:
		return "Cloud listener configuration exposes this endpoint and port."
	case caasmmodel.PortObservationSourceServiceEndpoint, caasmmodel.PortObservationSourceInferredWeb:
		return "Cloud service endpoint metadata implies this service port."
	default:
		if source == "" {
			source = "unknown"
		}
		return "Exposure reason is based on observation source: " + source + "."
	}
}

func caasmPortEvidenceKeys(entry caasmmodel.IPPortIndex, observation *repository.PortScannerObservation) []string {
	keys := []string{}
	switch entry.ObservationSource {
	case caasmmodel.PortObservationSourceInferredWeb, caasmmodel.PortObservationSourceServiceEndpoint, caasmmodel.PortObservationSourceListener:
		keys = append(keys, "inferred_web")
	case caasmmodel.PortObservationSourceSecurityGroup:
		keys = append(keys, "security_group_rule")
	}
	if observation != nil && strings.EqualFold(strings.TrimSpace(observation.Result), "confirmed_open") {
		keys = append(keys, "scanner_confirmed")
	}
	return keys
}

func caasmPrimaryPortEvidence(evidence []string) string {
	if len(evidence) == 0 {
		return ""
	}
	return evidence[0]
}

func caasmPortConfidence(evidence []string) string {
	for _, key := range evidence {
		if key == "scanner_confirmed" {
			return "high"
		}
	}
	for _, key := range evidence {
		switch key {
		case "security_group_rule":
			return "medium"
		case "inferred_web":
			return "low"
		}
	}
	return ""
}

func caasmPortSecurityGroupEvidence(entry caasmmodel.IPPortIndex) caasmPortEvidenceSectionDTO {
	securityGroups := jsonbStringSlice(entry.Asset.Metadata, "security_groups")
	metadata := map[string]interface{}{
		"observation_source": string(entry.ObservationSource),
	}
	if len(securityGroups) > 0 {
		metadata["security_groups"] = securityGroups
	}
	copyCaasmJSONBMetadata(metadata, entry.Evidence, "security_group_rule")
	copyCaasmJSONBMetadata(metadata, entry.Evidence, "rule")
	copyCaasmJSONBMetadata(metadata, entry.Evidence, "source")
	copyCaasmJSONBMetadata(metadata, entry.Evidence, "source_cidr")
	copyCaasmJSONBMetadata(metadata, entry.Evidence, "source_group")
	copyCaasmJSONBMetadata(metadata, entry.Evidence, "cidr")
	if entry.ObservationSource == caasmmodel.PortObservationSourceSecurityGroup {
		return caasmPortEvidenceSectionDTO{
			Available:  true,
			Title:      "Security group",
			Summary:    "Port was inferred from security group ingress. Exact rule CIDR/source detail is not persisted yet unless shown in metadata.",
			Source:     "security_group_rule",
			Confidence: "medium",
			Metadata:   metadata,
		}
	}
	return caasmPortEvidenceSectionDTO{
		Available: len(securityGroups) > 0,
		Title:     "Security group",
		Summary:   "No security-group rule evidence is attached to this port observation.",
		Metadata:  metadata,
	}
}

func caasmPortRouteEvidence(entry caasmmodel.IPPortIndex) caasmPortEvidenceSectionDTO {
	metadata := map[string]interface{}{}
	if vpcID := jsonbString(entry.Asset.Metadata, "vpc_id"); vpcID != "" {
		metadata["vpc_id"] = vpcID
	}
	if subnetID := jsonbString(entry.Asset.Metadata, "subnet_id"); subnetID != "" {
		metadata["subnet_id"] = subnetID
	}
	if isPublic, ok := jsonbBool(entry.Asset.Metadata, "is_public"); ok {
		metadata["is_public"] = isPublic
	}
	copyCaasmJSONBMetadata(metadata, entry.Asset.Metadata, "route_table_id")
	copyCaasmJSONBMetadata(metadata, entry.Asset.Metadata, "route_table")
	copyCaasmJSONBMetadata(metadata, entry.Asset.Metadata, "internet_gateway_id")
	copyCaasmJSONBMetadata(metadata, entry.Asset.Metadata, "egress")
	copyCaasmJSONBMetadata(metadata, entry.Asset.Metadata, "route")
	if len(metadata) == 0 {
		return caasmPortEvidenceSectionDTO{
			Available: false,
			Title:     "Route",
			Summary:   "Route evidence is not collected for this asset yet.",
		}
	}
	return caasmPortEvidenceSectionDTO{
		Available: true,
		Title:     "Route",
		Summary:   "Network metadata is available, but exact route-table and internet-gateway path evidence is not persisted yet.",
		Metadata:  metadata,
	}
}

func caasmPortEvidenceWarnings(entry caasmmodel.IPPortIndex) []string {
	warnings := []string{}
	if entry.ObservationSource == caasmmodel.PortObservationSourceSecurityGroup && jsonbString(entry.Evidence, "security_group_rule") == "" {
		warnings = append(warnings, "Security group rule detail is inferred, not exact-rule evidence.")
	}
	if entry.Exposure == "public" && entry.ObservationSource != caasmmodel.PortObservationSourceListener {
		warnings = append(warnings, "Public exposure should be confirmed with scanner result before treating it as reachable.")
	}
	return warnings
}

func copyCaasmJSONBMetadata(target map[string]interface{}, metadata coremodel.JSONB, key string) {
	if target == nil || metadata == nil {
		return
	}
	value, ok := metadata[key]
	if !ok || value == nil {
		return
	}
	target[key] = value
}

func jsonbString(metadata coremodel.JSONB, key string) string {
	value, ok := metadata[key]
	if !ok || value == nil {
		return ""
	}
	switch typed := value.(type) {
	case string:
		return strings.TrimSpace(typed)
	case *string:
		if typed == nil {
			return ""
		}
		return strings.TrimSpace(*typed)
	default:
		return strings.TrimSpace(fmt.Sprint(typed))
	}
}

func jsonbStringSlice(metadata coremodel.JSONB, key string) []string {
	value, ok := metadata[key]
	if !ok || value == nil {
		return nil
	}
	switch typed := value.(type) {
	case []string:
		return typed
	case []interface{}:
		out := make([]string, 0, len(typed))
		for _, item := range typed {
			text := strings.TrimSpace(fmt.Sprint(item))
			if text != "" {
				out = append(out, text)
			}
		}
		return out
	default:
		text := strings.TrimSpace(fmt.Sprint(typed))
		if text == "" {
			return nil
		}
		return []string{text}
	}
}

func firstNonEmptyString(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}

func jsonbBool(metadata coremodel.JSONB, key string) (bool, bool) {
	value, ok := metadata[key]
	if !ok || value == nil {
		return false, false
	}
	switch typed := value.(type) {
	case bool:
		return typed, true
	case *bool:
		if typed == nil {
			return false, false
		}
		return *typed, true
	default:
		normalized := strings.ToLower(strings.TrimSpace(fmt.Sprint(typed)))
		switch normalized {
		case "true", "1", "yes":
			return true, true
		case "false", "0", "no":
			return false, true
		default:
			return false, false
		}
	}
}

func stringValue(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}
