// Package auditcoverage 提供全系统入口的可复核审计覆盖扫描。
package auditcoverage

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"

	"github.com/bytedance/sonic"
)

// Entry 是一个必须经过审计分类的 HTTP、Worker 或定时任务入口。
type Entry struct {
	Key               string `json:"key"`
	Kind              string `json:"kind"`
	CodeEntry         string `json:"code_entry"`
	Owner             string `json:"owner"`
	Method            string `json:"method,omitempty"`
	Path              string `json:"path,omitempty"`
	Summary           string `json:"summary"`
	AuditEvent        string `json:"audit_event"`
	DomainLedger      string `json:"domain_ledger"`
	IntegrationLog    string `json:"integration_log"`
	Outbox            string `json:"outbox"`
	ActionCode        string `json:"action_code,omitempty"`
	ActionName        string `json:"action_name,omitempty"`
	Category          string `json:"category,omitempty"`
	Risk              string `json:"risk,omitempty"`
	PrimaryResource   string `json:"primary_resource,omitempty"`
	AffectedResource  string `json:"affected_resource,omitempty"`
	ActorSource       string `json:"actor_source"`
	Transaction       string `json:"transaction"`
	FailureStrategy   string `json:"failure_strategy"`
	SensitivePolicy   string `json:"sensitive_policy"`
	BeforeAfterPolicy string `json:"before_after_policy"`
	TestSeam          string `json:"test_seam"`
	NAReason          string `json:"na_reason,omitempty"`
}

// Scan 扫描当前仓库中对外 HTTP、Asynq Worker 和定时任务注册入口。
func Scan(root string) ([]Entry, error) {
	var entries []Entry
	files := []string{"internal/routes", "internal/application", "internal/domain", "internal/service", "pkg/queue", "cmd/worker"}
	for _, directory := range files {
		err := filepath.Walk(filepath.Join(root, directory), func(path string, info os.FileInfo, walkErr error) error {
			if walkErr != nil {
				return walkErr
			}
			if info.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
				return nil
			}
			found, err := scanFile(root, path)
			if err != nil {
				return err
			}
			entries = append(entries, found...)
			return nil
		})
		if err != nil {
			return nil, err
		}
	}
	sort.Slice(entries, func(i, j int) bool { return entries[i].Key < entries[j].Key })
	return entries, nil
}

// LoadManifest 读取经评审的显式覆盖快照。
func LoadManifest(path string) ([]Entry, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	var entries []Entry
	if err := sonic.Unmarshal(data, &entries); err != nil {
		return nil, err
	}
	return entries, nil
}

// MarshalManifest 将扫描结果输出为稳定、便于评审的 JSON。
func MarshalManifest(entries []Entry) ([]byte, error) {
	return sonic.ConfigStd.MarshalIndent(entries, "", "  ")
}

func scanFile(root, path string) ([]Entry, error) {
	set := token.NewFileSet()
	file, err := parser.ParseFile(set, path, nil, 0)
	if err != nil {
		return nil, err
	}
	relative, err := filepath.Rel(root, path)
	if err != nil {
		return nil, err
	}
	var entries []Entry
	ast.Inspect(file, func(node ast.Node) bool {
		call, ok := node.(*ast.CallExpr)
		if !ok {
			return true
		}
		position := set.Position(call.Pos())
		if identifier, ok := call.Fun.(*ast.Ident); ok && identifier.Name == "Register" && len(call.Args) >= 7 {
			method, methodOK := stringLiteral(call.Args[3])
			pathSuffix, pathOK := stringLiteral(call.Args[4])
			if methodOK && pathOK {
				entry := classifyHTTP(relative, position.Line, method, pathSuffix, expression(call.Args[5]), routeSummary(call.Args[6]))
				entries = append(entries, entry)
			}
			return true
		}
		selector, ok := call.Fun.(*ast.SelectorExpr)
		if !ok {
			return true
		}
		switch selector.Sel.Name {
		case "HandleFunc":
			if len(call.Args) >= 2 {
				entries = append(entries, classifyWorker(relative, position.Line, expression(call.Args[0]), expression(call.Args[1])))
			}
		case "Register":
			if strings.HasPrefix(relative, "cmd/worker/") {
				if taskType, schedule, ok := scheduledTask(call); ok {
					entries = append(entries, classifySchedule(relative, position.Line, taskType, schedule))
				}
			}
		}
		return true
	})
	if strings.HasPrefix(relative, "internal/application/") || strings.HasPrefix(relative, "internal/domain/") ||
		strings.HasPrefix(relative, "internal/service/") {
		for _, declaration := range file.Decls {
			function, ok := declaration.(*ast.FuncDecl)
			if !ok || function.Recv == nil || !isBusinessMethod(function.Name.Name) {
				continue
			}
			position := set.Position(function.Pos())
			entries = append(entries, classifyBusinessMethod(relative, position.Line, function.Name.Name))
		}
	}
	return entries, nil
}

func classifyHTTP(file string, line int, method, path, handler, summary string) Entry {
	owner := strings.TrimSuffix(filepath.Base(file), ".go")
	if summary == "" {
		summary = handler
	}
	entry := Entry{
		Key: fmt.Sprintf("http:%s:%d:%s:%s", file, line, method, path), Kind: "http",
		CodeEntry: fmt.Sprintf("%s:%d %s", file, line, handler), Owner: owner,
		Method: method, Path: path, Summary: summary, ActorSource: httpActorSource(file),
		DomainLedger: ledgerDecision(owner), IntegrationLog: integrationDecision(file, path),
		Outbox:            "按用例是否存在提交后可靠副作用决定；无可靠副作用时 N/A",
		SensitivePolicy:   "禁止字段删除；手机号、IP、ICCID、金额和第三方单号按权限脱敏；单字段 16KB 上限",
		BeforeAfterPolicy: "写操作保存脱敏后的直接字段变化；批量命令保存摘要和权威明细引用",
		TestSeam:          "真实 Fiber + Application/Service 公共用例 + PostgreSQL 事实；覆盖门禁静态比对本入口",
	}
	if method == "GET" && !isSensitiveRead(file, path, summary) {
		entry.AuditEvent = "N/A"
		entry.Transaction = "N/A"
		entry.FailureStrategy = "Access Log 记录统一错误；普通读取不创建业务审计"
		entry.NAReason = "普通只读查询，不改变业务事实且不返回需二次授权的完整敏感值"
		return entry
	}
	entry.AuditEvent = "必须"
	entry.ActionCode = actionCode(owner, handler)
	entry.ActionName = summary
	entry.Category = categoryFor(owner)
	entry.Risk = riskFor(owner, path, summary)
	entry.PrimaryResource = owner
	entry.AffectedResource = "由对应 Application/Service 用例按直接影响资源显式填写，禁止递归扩展"
	entry.Transaction = "成功事件与关键业务事实同一 GORM 事务；敏感读取在返回前写入"
	entry.FailureStrategy = "业务回滚后的 failed/denied 使用独立短事务；二次失败保留业务错误并记录 critical"
	return entry
}

func classifyWorker(file string, line int, taskType, handler string) Entry {
	owner := workerOwner(taskType)
	entry := Entry{
		Key: fmt.Sprintf("worker:%s:%d:%s", file, line, taskType), Kind: "worker",
		CodeEntry: fmt.Sprintf("%s:%d %s", file, line, handler), Owner: owner,
		Summary: "处理异步任务 " + taskType, AuditEvent: "按状态变化、人工触发、连续失败或高风险异常决定",
		DomainLedger: ledgerDecision(owner), IntegrationLog: workerIntegrationDecision(taskType),
		Outbox:     "任务来源 Outbox/业务任务事实；消费端按稳定事件或任务 ID 幂等",
		ActionCode: actionCode(owner, handler), ActionName: "处理异步任务（" + taskType + "）",
		Category: categoryFor(owner), Risk: riskFor(owner, taskType, handler),
		PrimaryResource: owner, AffectedResource: "任务载荷定位的直接业务资源",
		ActorSource: "system_task/asynq", Transaction: "业务状态变化、领域流水和 Audit Event 按用例原子提交",
		FailureStrategy:   "Worker 返回错误由公共重试恢复；终态失败保存中文安全摘要，禁止裸 goroutine 审计",
		SensitivePolicy:   "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL",
		BeforeAfterPolicy: "状态变化保存直接前后值；无业务变化时仅保留 Integration Log",
		TestSeam:          "公开 Asynq Handler + PostgreSQL/Redis 事实 + 重复消费测试；覆盖门禁静态比对本入口",
	}
	return entry
}

func classifySchedule(file string, line int, taskType, schedule string) Entry {
	return Entry{
		Key: fmt.Sprintf("schedule:%s:%d:%s", file, line, taskType), Kind: "scheduled_job",
		CodeEntry: fmt.Sprintf("%s:%d", file, line), Owner: workerOwner(taskType), Summary: "按 " + schedule + " 调度 " + taskType,
		AuditEvent: "N/A", DomainLedger: "N/A", IntegrationLog: "N/A", Outbox: "N/A",
		ActorSource: "system_task/scheduled_job", Transaction: "N/A",
		FailureStrategy:   "调度注册失败阻止 Worker 启动；执行结果由对应 Worker 入口负责",
		SensitivePolicy:   "调度日志仅记录任务类型与安全时间信息",
		BeforeAfterPolicy: "N/A：调度入口不修改业务事实",
		TestSeam:          "调度注册公开函数 + 覆盖门禁静态比对本入口",
		NAReason:          "本入口只产生调度信号，不直接读取或修改业务事实；审计责任位于对应 Worker",
	}
}

func classifyBusinessMethod(file string, line int, method string) Entry {
	parts := strings.Split(file, "/")
	layer := parts[1]
	owner := strings.TrimSuffix(filepath.Base(filepath.Dir(file)), ".go")
	if owner == "service" || owner == "application" || owner == "domain" {
		owner = strings.TrimSuffix(filepath.Base(file), ".go")
	}
	entry := Entry{
		Key: fmt.Sprintf("%s:%s:%d:%s", layer, file, line, method), Kind: layer,
		CodeEntry: fmt.Sprintf("%s:%d %s", file, line, method), Owner: owner,
		Summary: "业务方法 " + method, DomainLedger: ledgerDecision(owner),
		IntegrationLog: businessIntegrationDecision(file, method),
		Outbox:         "存在提交后可靠副作用时必须在同一事务追加；否则 N/A",
		ActionCode:     actionCode(owner, method), Risk: riskFor(owner, file, method),
		PrimaryResource: owner, AffectedResource: "完整用例直接修改或引用的资源",
		ActorSource:       "由调用入口传入操作者与来源快照",
		SensitivePolicy:   "禁止字段删除；受控字段脱敏；批量明细留在领域任务或制品",
		BeforeAfterPolicy: "完整用例保存脱敏后的直接业务变化；Domain 方法由 Application 投影",
		TestSeam:          "Application/Service 公共方法 + PostgreSQL 事实；Domain 使用纯领域测试；覆盖门禁静态比对本入口",
	}
	if layer == "domain" {
		entry.AuditEvent = "N/A"
		entry.Transaction = "由 Application 组合根负责"
		entry.FailureStrategy = "返回领域错误，由 Application 在回滚后裁决 failed/denied 审计"
		entry.NAReason = "Domain 只维护业务不变量和领域事实，不依赖审计基础设施；Audit Event 由 Application 写入"
		entry.ActionCode = ""
		entry.ActionName = ""
		entry.Category = ""
		entry.Risk = ""
		entry.PrimaryResource = ""
		return entry
	}
	entry.AuditEvent = "必须"
	entry.ActionName = "执行业务方法（" + method + "）"
	entry.Category = categoryFor(owner)
	entry.Transaction = "关键成功事件与业务事实同一 GORM 事务"
	entry.FailureStrategy = "业务回滚后的 failed/denied 使用独立短事务；审计二次失败记录 critical"
	return entry
}

func routeSummary(expr ast.Expr) string {
	composite, ok := expr.(*ast.CompositeLit)
	if !ok {
		return ""
	}
	for _, element := range composite.Elts {
		pair, ok := element.(*ast.KeyValueExpr)
		if !ok || expression(pair.Key) != "Summary" {
			continue
		}
		value, _ := stringLiteral(pair.Value)
		return value
	}
	return ""
}

func scheduledTask(call *ast.CallExpr) (string, string, bool) {
	if len(call.Args) < 2 {
		return "", "", false
	}
	schedule, _ := stringLiteral(call.Args[0])
	taskCall, ok := call.Args[1].(*ast.CallExpr)
	if !ok {
		return "", "", false
	}
	selector, ok := taskCall.Fun.(*ast.SelectorExpr)
	if !ok || selector.Sel.Name != "NewTask" || len(taskCall.Args) == 0 {
		return "", "", false
	}
	return expression(taskCall.Args[0]), schedule, true
}

func stringLiteral(expr ast.Expr) (string, bool) {
	literal, ok := expr.(*ast.BasicLit)
	if !ok || literal.Kind != token.STRING {
		return "", false
	}
	value, err := strconv.Unquote(literal.Value)
	return value, err == nil
}

func expression(expr ast.Expr) string {
	switch value := expr.(type) {
	case *ast.Ident:
		return value.Name
	case *ast.SelectorExpr:
		return expression(value.X) + "." + value.Sel.Name
	case *ast.BasicLit:
		return value.Value
	case *ast.CallExpr:
		return expression(value.Fun)
	default:
		return fmt.Sprintf("%T", expr)
	}
}

func httpActorSource(file string) string {
	switch {
	case strings.HasSuffix(file, "personal.go"):
		return "personal_customer/personal_api"
	case strings.HasSuffix(file, "order.go"):
		return "按路由分为 admin_user/admin_api 或外部回调入口"
	default:
		return "登录账号快照/admin_api"
	}
}

func isSensitiveRead(file, path, summary string) bool {
	text := strings.ToLower(file + " " + path + " " + summary)
	for _, marker := range []string{"download", "export", "realname-link", "实名", "敏感", "完整", "operation-password"} {
		if strings.Contains(text, marker) {
			return true
		}
	}
	return false
}

func integrationDecision(file, path string) string {
	text := strings.ToLower(file + " " + path)
	if strings.Contains(text, "callback") || strings.HasSuffix(file, "order.go") &&
		(strings.Contains(path, "pay") || strings.Contains(path, "alipay")) {
		return "必须：业务处理前保存入站安全摘要与幂等标识"
	}
	return "无外部交互时 N/A；用例调用 Gateway、支付、企微或运营商时必须"
}

func workerIntegrationDecision(taskType string) string {
	text := strings.ToLower(taskType)
	if strings.Contains(text, "polling") {
		return "必须：每次实际请求或未发送裁决均记录"
	}
	return "Worker 调用外部系统时必须；纯本地处理 N/A"
}

func businessIntegrationDecision(file, method string) string {
	text := strings.ToLower(file + " " + method)
	for _, marker := range []string{"polling", "gateway", "payment", "wechat", "wecom", "carrier", "sms"} {
		if strings.Contains(text, marker) {
			return "调用外部系统或处理回调时必须；纯本地分支 N/A"
		}
	}
	return "N/A：当前方法按代码位置属于本地业务用例；后续新增外部调用必须重新分类"
}

func ledgerDecision(owner string) string {
	for _, marker := range []string{"order", "recharge", "refund", "commission", "wallet"} {
		if strings.Contains(owner, marker) {
			return "必须：订单、充值、退款、钱包流水等既有业务表是领域权威"
		}
	}
	if strings.Contains(owner, "import") || strings.Contains(owner, "export") {
		return "业务任务及明细表是批量结果权威"
	}
	return "既有业务表是状态事实；Audit Event 不替代业务模型"
}

func riskFor(owner, path, summary string) string {
	text := strings.ToLower(owner + " " + path + " " + summary)
	for _, marker := range []string{"wallet", "refund", "recharge", "permission", "role", "password", "config", "权限", "资金", "退款", "充值"} {
		if strings.Contains(text, marker) {
			return "high"
		}
	}
	return "normal"
}

func categoryFor(owner string) string {
	text := strings.ToLower(owner)
	for _, marker := range []string{"wallet", "refund", "recharge", "commission", "order"} {
		if strings.Contains(text, marker) {
			return "finance"
		}
	}
	for _, marker := range []string{"account", "role", "permission", "auth"} {
		if strings.Contains(text, marker) {
			return "security"
		}
	}
	for _, marker := range []string{"card", "device", "asset", "polling"} {
		if strings.Contains(text, marker) {
			return "asset"
		}
	}
	return "business"
}

func actionCode(owner, handler string) string {
	method := handler
	if index := strings.LastIndex(method, "."); index >= 0 {
		method = method[index+1:]
	}
	return normalize(owner) + "." + normalize(method)
}

func workerOwner(taskType string) string {
	return normalize(strings.TrimPrefix(taskType, "constants.TaskType"))
}

func isBusinessMethod(name string) bool {
	for _, prefix := range []string{
		"Create", "Update", "Delete", "Set", "Assign", "Remove", "Cancel", "Reject", "Approve",
		"Import", "Allocate", "Recall", "Stop", "Resume", "Bind", "Unbind", "Reset", "Activate",
		"Deactivate", "Trigger", "Handle", "Process", "Execute", "Replay", "Release", "Change", "Pay",
		"Refund", "Recharge", "Withdraw", "Grant", "Revoke", "Deduct", "Credit", "Debit", "Freeze",
		"Unfreeze", "Resolve", "Expire", "Invalidate", "Archive", "Cleanup", "Adjust", "Add", "Batch",
		"Enable", "Disable", "Login", "Logout", "Refresh", "Upload", "Download", "Save", "Restore",
		"Submit", "Sync", "Migrate", "Send",
	} {
		if strings.HasPrefix(name, prefix) {
			return true
		}
	}
	return false
}

func normalize(value string) string {
	value = strings.Trim(value, "\"")
	var output []rune
	for index, current := range []rune(value) {
		if current >= 'A' && current <= 'Z' {
			if index > 0 {
				output = append(output, '_')
			}
			current += 'a' - 'A'
		}
		if current == '-' || current == ':' || current == '/' {
			current = '_'
		}
		output = append(output, current)
	}
	return strings.Trim(strings.ReplaceAll(string(output), "__", "_"), "_")
}
