package handler

import (
	"errors"
	"net/http"
	"strconv"
	"strings"
	"time"

	"cyberstrike-ai/internal/database"
	"cyberstrike-ai/internal/security"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
)

type AssetHandler struct {
	db     *database.DB
	logger *zap.Logger
}

const (
	maxAssetImportBatch    = 100000
	maxAssetOperationBatch = 10000
)

func NewAssetHandler(db *database.DB, logger *zap.Logger) *AssetHandler {
	return &AssetHandler{db: db, logger: logger}
}

func assetAccess(c *gin.Context) database.RBACListAccess {
	if session, ok := security.CurrentSession(c); ok {
		return database.RBACListAccess{UserID: session.UserID, Scope: session.Scope}
	}
	return database.RBACListAccess{}
}

func assetAccessForPermission(c *gin.Context, permission string) database.RBACListAccess {
	if session, ok := security.CurrentSession(c); ok {
		return database.RBACListAccess{UserID: session.UserID, Scope: session.ScopeFor(permission)}
	}
	return database.RBACListAccess{}
}

type importAssetsRequest struct {
	Assets      []*database.Asset `json:"assets" binding:"required"`
	Source      string            `json:"source"`
	SourceQuery string            `json:"source_query"`
}

type assetScanLink struct {
	AssetID        string `json:"asset_id" binding:"required"`
	ConversationID string `json:"conversation_id"`
	QueueID        string `json:"queue_id"`
	TaskID         string `json:"task_id"`
}

type recordAssetScansRequest struct {
	Scans []assetScanLink `json:"scans" binding:"required"`
}

type updateAssetsProjectRequest struct {
	AssetIDs  []string `json:"asset_ids" binding:"required"`
	ProjectID string   `json:"project_id"`
}

type bulkUpdateAssetsRequest struct {
	AssetIDs          []string `json:"asset_ids" binding:"required"`
	Status            *string  `json:"status"`
	ResponsiblePerson *string  `json:"responsible_person"`
	Department        *string  `json:"department"`
	BusinessSystem    *string  `json:"business_system"`
	Environment       *string  `json:"environment"`
	Criticality       *string  `json:"criticality"`
	AddTags           []string `json:"add_tags"`
	RemoveTags        []string `json:"remove_tags"`
}

type assetIDsRequest struct {
	AssetIDs []string `json:"asset_ids" binding:"required"`
}

type mergeAssetsRequest struct {
	AssetIDs  []string `json:"asset_ids" binding:"required"`
	PrimaryID string   `json:"primary_id"`
}

func (h *AssetHandler) Import(c *gin.Context) {
	var req importAssetsRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if len(req.Assets) == 0 || len(req.Assets) > maxAssetImportBatch {
		c.JSON(http.StatusBadRequest, gin.H{"error": "assets 数量必须在 1-100000 之间"})
		return
	}
	owner := ""
	allowGlobal := false
	if session, ok := security.CurrentSession(c); ok {
		owner = session.UserID
		allowGlobal = session.Scope == database.RBACScopeAll
	}
	for _, asset := range req.Assets {
		if asset == nil {
			continue
		}
		if strings.TrimSpace(asset.ProjectID) != "" {
			if session, ok := security.CurrentSession(c); ok && !h.db.UserCanAccessResource(session.UserID, session.Scope, "project", strings.TrimSpace(asset.ProjectID)) {
				c.JSON(http.StatusForbidden, gin.H{"error": "无权绑定该项目"})
				return
			}
		}
		if strings.TrimSpace(asset.Source) == "" {
			asset.Source = strings.TrimSpace(req.Source)
		}
		if strings.TrimSpace(asset.SourceQuery) == "" {
			asset.SourceQuery = strings.TrimSpace(req.SourceQuery)
		}
	}
	result, err := h.db.UpsertAssets(req.Assets, owner, allowGlobal)
	if err != nil {
		var validationErr *database.AssetValidationError
		if errors.As(err, &validationErr) {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		h.logger.Error("导入资产失败", zap.Error(err))
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, result)
}

func (h *AssetHandler) List(c *gin.Context) {
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
	if page < 1 {
		page = 1
	}
	if pageSize < 1 || pageSize > 100 {
		pageSize = 20
	}
	filter, err := assetListFilterFromQuery(c)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	assets, total, err := h.db.ListAssets(pageSize, (page-1)*pageSize, filter, assetAccess(c))
	if err != nil {
		h.logger.Error("加载资产失败", zap.Error(err))
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	totalPages := (total + pageSize - 1) / pageSize
	if totalPages < 1 {
		totalPages = 1
	}
	c.JSON(http.StatusOK, gin.H{"assets": assets, "total": total, "page": page, "page_size": pageSize, "total_pages": totalPages})
}

func assetListFilterFromQuery(c *gin.Context) (database.AssetListFilter, error) {
	filter := database.AssetListFilter{
		Search: strings.TrimSpace(c.Query("q")), Status: strings.ToLower(strings.TrimSpace(c.Query("status"))),
		Protocol: strings.ToLower(strings.TrimSpace(c.Query("protocol"))), ProjectID: strings.TrimSpace(c.Query("project_id")),
		Source: strings.TrimSpace(c.Query("source")), Tag: strings.TrimSpace(c.Query("tag")), Host: strings.TrimSpace(c.Query("host")),
		IP: strings.TrimSpace(c.Query("ip")), Domain: strings.TrimSpace(c.Query("domain")), ScanState: strings.ToLower(strings.TrimSpace(c.Query("scan_state"))),
		SortBy: strings.ToLower(strings.TrimSpace(c.Query("sort_by"))), SortOrder: strings.ToLower(strings.TrimSpace(c.Query("sort_order"))),
		RiskLevel: strings.ToLower(strings.TrimSpace(c.Query("risk_level"))),
		Country:   strings.TrimSpace(c.Query("country")), Province: strings.TrimSpace(c.Query("province")), City: strings.TrimSpace(c.Query("city")),
		ResponsiblePerson: strings.TrimSpace(c.Query("responsible_person")), Department: strings.TrimSpace(c.Query("department")),
		BusinessSystem: strings.TrimSpace(c.Query("business_system")), Environment: strings.ToLower(strings.TrimSpace(c.Query("environment"))),
		Criticality: strings.ToLower(strings.TrimSpace(c.Query("criticality"))),
	}
	if raw := strings.TrimSpace(c.Query("port")); raw != "" {
		port, err := strconv.Atoi(raw)
		if err != nil || port < 0 || port > 65535 {
			return filter, &assetQueryError{field: "port", value: raw}
		}
		filter.Port = &port
	}
	for field, target := range map[string]**int{
		"min_vulnerabilities": &filter.MinVulnerabilities,
		"max_vulnerabilities": &filter.MaxVulnerabilities,
		"scan_overdue_days":   &filter.ScanOverdueDays,
	} {
		raw := strings.TrimSpace(c.Query(field))
		if raw == "" {
			continue
		}
		value, err := strconv.Atoi(raw)
		if err != nil || value < 0 || (field == "scan_overdue_days" && value == 0) {
			return filter, &assetQueryError{field: field, value: raw}
		}
		*target = &value
	}
	var err error
	if filter.LastScanBefore, err = parseAssetQueryTime("last_scan_before", c.Query("last_scan_before")); err != nil {
		return filter, err
	}
	if filter.LastScanAfter, err = parseAssetQueryTime("last_scan_after", c.Query("last_scan_after")); err != nil {
		return filter, err
	}
	if filter.FirstSeenBefore, err = parseAssetQueryTime("first_seen_before", c.Query("first_seen_before")); err != nil {
		return filter, err
	}
	if filter.FirstSeenAfter, err = parseAssetQueryTime("first_seen_after", c.Query("first_seen_after")); err != nil {
		return filter, err
	}
	if filter.LastSeenBefore, err = parseAssetQueryTime("last_seen_before", c.Query("last_seen_before")); err != nil {
		return filter, err
	}
	if filter.LastSeenAfter, err = parseAssetQueryTime("last_seen_after", c.Query("last_seen_after")); err != nil {
		return filter, err
	}
	return filter, nil
}

// Selection resolves all assets matching the current filter for cross-page actions.
func (h *AssetHandler) Selection(c *gin.Context) {
	filter, err := assetListFilterFromQuery(c)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	assets, total, err := h.db.ListAssetsForOperation(maxAssetOperationBatch, filter, assetAccess(c))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "total": total})
		return
	}
	c.JSON(http.StatusOK, gin.H{"assets": assets, "total": total})
}

type assetQueryError struct{ field, value string }

func (e *assetQueryError) Error() string {
	return e.field + " 参数无效: " + e.value
}

func parseAssetQueryTime(field, value string) (*time.Time, error) {
	value = strings.TrimSpace(value)
	if value == "" {
		return nil, nil
	}
	for _, layout := range []string{time.RFC3339, "2006-01-02"} {
		if parsed, err := time.Parse(layout, value); err == nil {
			return &parsed, nil
		}
	}
	return nil, &assetQueryError{field: field, value: value}
}

func (h *AssetHandler) Stats(c *gin.Context) {
	days := 30
	if raw := strings.TrimSpace(c.Query("days")); raw != "" {
		parsed, err := strconv.Atoi(raw)
		if err != nil || (parsed != 7 && parsed != 30 && parsed != 90) {
			c.JSON(http.StatusBadRequest, gin.H{"error": "days 仅支持 7、30 或 90"})
			return
		}
		days = parsed
	}
	stats, err := h.db.GetAssetStats(assetAccess(c), days)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, stats)
}

// RecordScans stores the execution link created by the asset-library scan action.
func (h *AssetHandler) RecordScans(c *gin.Context) {
	var req recordAssetScansRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if len(req.Scans) == 0 || len(req.Scans) > maxAssetOperationBatch {
		c.JSON(http.StatusBadRequest, gin.H{"error": "scans 数量必须在 1-10000 之间"})
		return
	}
	access := assetAccess(c)
	for _, scan := range req.Scans {
		conversationID := strings.TrimSpace(scan.ConversationID)
		queueID := strings.TrimSpace(scan.QueueID)
		taskID := strings.TrimSpace(scan.TaskID)
		if conversationID == "" && taskID == "" {
			c.JSON(http.StatusBadRequest, gin.H{"error": "conversation_id 或 task_id 至少需要一个"})
			return
		}
		if taskID != "" && (queueID == "" || !h.db.BatchTaskBelongsToQueue(taskID, queueID)) {
			c.JSON(http.StatusBadRequest, gin.H{"error": "任务不属于指定队列"})
			return
		}
		if _, err := h.db.GetAsset(strings.TrimSpace(scan.AssetID), access); err != nil {
			c.JSON(http.StatusForbidden, gin.H{"error": "资产不存在或无权扫描"})
			return
		}
		if session, ok := security.CurrentSession(c); ok {
			if id := conversationID; id != "" && !h.db.UserCanAccessResource(session.UserID, session.Scope, "conversation", id) {
				c.JSON(http.StatusForbidden, gin.H{"error": "无权关联该对话"})
				return
			}
			if id := queueID; id != "" && !h.db.UserCanAccessResource(session.UserID, session.Scope, "batch_task", id) {
				c.JSON(http.StatusForbidden, gin.H{"error": "无权关联该任务队列"})
				return
			}
		}
	}
	for _, scan := range req.Scans {
		if err := h.db.MarkAssetScanned(scan.AssetID, scan.ConversationID, scan.QueueID, scan.TaskID, access); err != nil {
			h.logger.Error("记录资产扫描失败", zap.String("asset_id", scan.AssetID), zap.Error(err))
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
	}
	c.JSON(http.StatusOK, gin.H{"updated": len(req.Scans)})
}

func (h *AssetHandler) Update(c *gin.Context) {
	var asset database.Asset
	if err := c.ShouldBindJSON(&asset); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if asset.ProjectID != "" {
		if session, ok := security.CurrentSession(c); ok && !h.db.UserCanAccessResource(session.UserID, session.Scope, "project", asset.ProjectID) {
			c.JSON(http.StatusForbidden, gin.H{"error": "无权绑定该项目"})
			return
		}
	}
	if err := h.db.UpdateAsset(c.Param("id"), &asset, assetAccess(c)); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	updated, err := h.db.GetAsset(c.Param("id"), assetAccess(c))
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "资产不存在"})
		return
	}
	c.JSON(http.StatusOK, updated)
}

// UpdateProjectBinding replaces the project binding for a selected asset set.
func (h *AssetHandler) UpdateProjectBinding(c *gin.Context) {
	var req updateAssetsProjectRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if len(req.AssetIDs) == 0 || len(req.AssetIDs) > maxAssetOperationBatch {
		c.JSON(http.StatusBadRequest, gin.H{"error": "asset_ids 数量必须在 1-10000 之间"})
		return
	}
	req.ProjectID = strings.TrimSpace(req.ProjectID)
	if req.ProjectID != "" {
		if _, err := h.db.GetProject(req.ProjectID); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "项目不存在"})
			return
		}
		if session, ok := security.CurrentSession(c); ok && !h.db.UserCanAccessResource(session.UserID, session.Scope, "project", req.ProjectID) {
			c.JSON(http.StatusForbidden, gin.H{"error": "无权绑定该项目"})
			return
		}
	}
	updated, err := h.db.UpdateAssetsProject(req.AssetIDs, req.ProjectID, assetAccess(c))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, gin.H{"updated": updated, "project_id": req.ProjectID})
}

func (h *AssetHandler) BulkUpdate(c *gin.Context) {
	var req bulkUpdateAssetsRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if len(req.AssetIDs) == 0 || len(req.AssetIDs) > maxAssetOperationBatch {
		c.JSON(http.StatusBadRequest, gin.H{"error": "asset_ids 数量必须在 1-10000 之间"})
		return
	}
	updated, err := h.db.UpdateAssetsBulk(req.AssetIDs, database.AssetBulkPatch{
		Status: req.Status, ResponsiblePerson: req.ResponsiblePerson, Department: req.Department,
		BusinessSystem: req.BusinessSystem, Environment: req.Environment, Criticality: req.Criticality,
		AddTags: req.AddTags, RemoveTags: req.RemoveTags,
	}, assetAccess(c))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, gin.H{"updated": updated})
}

func (h *AssetHandler) BatchDelete(c *gin.Context) {
	var req assetIDsRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if len(req.AssetIDs) == 0 || len(req.AssetIDs) > maxAssetOperationBatch {
		c.JSON(http.StatusBadRequest, gin.H{"error": "asset_ids 数量必须在 1-10000 之间"})
		return
	}
	deleted, err := h.db.DeleteAssets(req.AssetIDs, assetAccess(c))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, gin.H{"deleted": deleted})
}

func assetIdentityKeys(asset *database.Asset) map[string]struct{} {
	keys := map[string]struct{}{}
	if value := strings.ToLower(strings.TrimSpace(asset.Domain)); value != "" {
		keys["domain:"+value] = struct{}{}
	}
	if value := strings.ToLower(strings.Trim(strings.TrimSpace(asset.IP), "[]")); value != "" {
		keys["ip:"+value] = struct{}{}
	}
	if value := strings.ToLower(strings.TrimSpace(asset.Host)); value != "" {
		keys["host:"+value] = struct{}{}
	}
	return keys
}

func shareAssetIdentity(left, right *database.Asset) bool {
	for key := range assetIdentityKeys(left) {
		if _, ok := assetIdentityKeys(right)[key]; ok {
			return true
		}
	}
	return false
}

// Merge keeps the selected primary asset and safely combines compatible duplicate metadata.
func (h *AssetHandler) Merge(c *gin.Context) {
	var req mergeAssetsRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if len(req.AssetIDs) < 2 || len(req.AssetIDs) > 100 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "合并资产数量必须在 2-100 之间"})
		return
	}
	writeAccess := assetAccessForPermission(c, "asset:write")
	deleteAccess := assetAccessForPermission(c, "asset:delete")
	primaryID := strings.TrimSpace(req.PrimaryID)
	if primaryID == "" {
		primaryID = strings.TrimSpace(req.AssetIDs[0])
	}
	primary, err := h.db.GetAsset(primaryID, writeAccess)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "主资产不存在或无权访问"})
		return
	}
	others := make([]*database.Asset, 0, len(req.AssetIDs)-1)
	seen := map[string]struct{}{primaryID: {}}
	for _, id := range req.AssetIDs {
		id = strings.TrimSpace(id)
		if id == "" || id == primaryID {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		item, err := h.db.GetAsset(id, writeAccess)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "部分资产不存在或无权访问"})
			return
		}
		if !shareAssetIdentity(primary, item) {
			c.JSON(http.StatusBadRequest, gin.H{"error": "所选资产没有共同域名、IP 或 Host，不能判定为重复资产"})
			return
		}
		others = append(others, item)
	}
	if len(others) == 0 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "至少需要两个不同资产"})
		return
	}
	mergeText := func(dst *string, src string) {
		if strings.TrimSpace(*dst) == "" && strings.TrimSpace(src) != "" {
			*dst = src
		}
	}
	tagSet := map[string]struct{}{}
	for _, tag := range primary.Tags {
		tagSet[tag] = struct{}{}
	}
	for _, item := range others {
		mergeText(&primary.ProjectID, item.ProjectID)
		mergeText(&primary.Host, item.Host)
		mergeText(&primary.IP, item.IP)
		mergeText(&primary.Domain, item.Domain)
		mergeText(&primary.Protocol, item.Protocol)
		mergeText(&primary.Title, item.Title)
		mergeText(&primary.Server, item.Server)
		mergeText(&primary.Country, item.Country)
		mergeText(&primary.Province, item.Province)
		mergeText(&primary.City, item.City)
		mergeText(&primary.ResponsiblePerson, item.ResponsiblePerson)
		mergeText(&primary.Department, item.Department)
		mergeText(&primary.BusinessSystem, item.BusinessSystem)
		mergeText(&primary.Environment, item.Environment)
		mergeText(&primary.Criticality, item.Criticality)
		for _, tag := range item.Tags {
			tagSet[tag] = struct{}{}
		}
	}
	primary.Tags = primary.Tags[:0]
	for tag := range tagSet {
		primary.Tags = append(primary.Tags, tag)
	}
	if len(primary.Tags) > 30 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "合并后标签超过 30 个"})
		return
	}
	ids := make([]string, 0, len(others))
	for _, item := range others {
		ids = append(ids, item.ID)
	}
	merged, err := h.db.MergeAssets(primary, ids, writeAccess, deleteAccess)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	updated, _ := h.db.GetAsset(primary.ID, writeAccess)
	c.JSON(http.StatusOK, gin.H{"merged": merged, "asset": updated})
}

func (h *AssetHandler) Delete(c *gin.Context) {
	if err := h.db.DeleteAsset(c.Param("id"), assetAccess(c)); err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "资产不存在或无权删除"})
		return
	}
	c.JSON(http.StatusOK, gin.H{"success": true})
}
