package main

import (
	"bytes"
	"encoding/json"
	"encoding/xml"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"sort"
	"strings"

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

const (
	sourceTypeOfficialPack = "OfficialPack"

	sourcePackWorkflowAlertKind = "workflow_alert"
)

type sourcePackApprovalProcessXML struct {
	XMLName                  xml.Name                      `xml:"ApprovalProcess"`
	InitialSubmissionActions *sourcePackApprovalActionsXML `xml:"initialSubmissionActions"`
	StepApprovalActions      *sourcePackApprovalActionsXML `xml:"stepApprovalActions"`
	FinalApprovalActions     *sourcePackApprovalActionsXML `xml:"finalApprovalActions"`
	FinalRejectionActions    *sourcePackApprovalActionsXML `xml:"finalRejectionActions"`
	RecallActions            *sourcePackApprovalActionsXML `xml:"recallActions"`
	ReassignActions          *sourcePackApprovalActionsXML `xml:"reassignActions"`
	Steps                    []sourcePackApprovalStepXML   `xml:"approvalStep"`
}

type sourcePackApprovalStepXML struct {
	Name                 string                        `xml:"name"`
	StepApprovalActions  *sourcePackApprovalActionsXML `xml:"stepApprovalActions"`
	StepRejectionActions *sourcePackApprovalActionsXML `xml:"stepRejectionActions"`
}

type sourcePackApprovalActionsXML struct {
	Action  []sourcePackApprovalActionXML `xml:"action"`
	Actions []sourcePackApprovalActionXML `xml:"actions"`
}

type sourcePackApprovalActionXML struct {
	Name            string `xml:"name"`
	Type            string `xml:"type"`
	EmailTemplateID string `xml:"emailTemplateId"`
	APIName         string `xml:"apiName"`
}

type sourcePackApprovalActionFinding struct {
	Name            string
	Type            string
	EmailTemplateID string
	APIName         string
	Event           string
	StepName        string
	Order           int
}

func (r *Report) appendOfficialPackVerifyZeroDependencies(officialPackRoot string, gate *VerifyZeroGate, dependencies *[]VerifyZeroDependency) {
	root := strings.TrimSpace(officialPackRoot)
	gate.OfficialPackRoot = root
	if root == "" {
		r.addOfficialPackScanBlocker(
			officialAppPacksRootEnvVar,
			"official_pack_root_missing",
			fmt.Sprintf("%s is required for verify-zero so the gate proves official source-backed packs cannot reintroduce WorkflowAlert dependencies.", officialAppPacksRootEnvVar),
			"Set NEXUSCRM_OFFICIAL_APP_PACKS_ROOT to the official nexuscrm-app-packs checkout and rerun verify-zero.",
		)
		return
	}

	rootAbs, err := filepath.Abs(root)
	if err != nil {
		r.addOfficialPackScanBlocker(root, "official_pack_root_unavailable", fmt.Sprintf("resolve official pack root: %v", err), "Fix the official pack root path and rerun verify-zero.")
		return
	}
	info, err := os.Stat(rootAbs)
	if err != nil {
		r.addOfficialPackScanBlocker(rootAbs, "official_pack_root_unavailable", fmt.Sprintf("stat official pack root: %v", err), "Fix the official pack root path and rerun verify-zero.")
		return
	}
	if !info.IsDir() {
		r.addOfficialPackScanBlocker(rootAbs, "official_pack_root_unavailable", "official pack root is not a directory", "Point NEXUSCRM_OFFICIAL_APP_PACKS_ROOT at the nexuscrm-app-packs checkout root and rerun verify-zero.")
		return
	}

	manifestPaths, err := verifyZeroPackManifestPaths(rootAbs)
	if err != nil {
		r.addOfficialPackScanBlocker(rootAbs, "official_pack_scan_failed", fmt.Sprintf("scan official pack manifests: %v", err), "Fix official pack source access and rerun verify-zero.")
		return
	}
	gate.ScannedOfficialPackCount = len(manifestPaths)
	if len(manifestPaths) == 0 {
		r.addOfficialPackScanBlocker(rootAbs, "official_pack_scan_empty", "no pack.json files were found under the official pack root", "Point NEXUSCRM_OFFICIAL_APP_PACKS_ROOT at a nexuscrm-app-packs checkout with official pack manifests and rerun verify-zero.")
		return
	}

	for _, manifestPath := range manifestPaths {
		r.scanOfficialPackVerifyZeroManifest(rootAbs, manifestPath, dependencies)
	}
}

func verifyZeroPackManifestPaths(root string) ([]string, error) {
	var paths []string
	err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if entry.IsDir() {
			if path != root && verifyZeroSkipPackScanDir(entry.Name()) {
				return filepath.SkipDir
			}
			return nil
		}
		if entry.Name() == "pack.json" {
			paths = append(paths, path)
		}
		return nil
	})
	sort.Strings(paths)
	return paths, err
}

func verifyZeroSkipPackScanDir(name string) bool {
	if strings.HasPrefix(name, ".") {
		return true
	}
	switch name {
	case "node_modules", "dist", "build", "generated-pack":
		return true
	default:
		return false
	}
}

func (r *Report) scanOfficialPackVerifyZeroManifest(rootAbs, manifestPath string, dependencies *[]VerifyZeroDependency) {
	raw, err := os.ReadFile(manifestPath)
	sourceKey := officialPackSourceKey(rootAbs, manifestPath)
	if err != nil {
		r.addOfficialPackScanBlocker(sourceKey, "official_pack_manifest_read_failed", fmt.Sprintf("read pack manifest: %v", err), "Fix official pack source access and rerun verify-zero.")
		return
	}
	var manifest packManifest
	dec := json.NewDecoder(bytes.NewReader(raw))
	dec.DisallowUnknownFields()
	if err := dec.Decode(&manifest); err != nil {
		r.addOfficialPackScanBlocker(sourceKey, "official_pack_manifest_parse_failed", fmt.Sprintf("parse pack manifest: %v", err), "Fix the pack manifest before using verify-zero as cutover evidence.")
		return
	}
	packName := strings.TrimSpace(manifest.Name)
	if packName == "" {
		packName = filepath.Base(filepath.Dir(manifestPath))
	}
	packRoot := filepath.Dir(manifestPath)

	for _, component := range manifest.Components {
		switch {
		case sourcePackComponentKindEquals(component.Kind, sourcePackWorkflowAlertKind):
			r.appendSourcePackWorkflowAlertDependency(packName, component, dependencies)
		case sourcePackComponentKindEquals(component.Kind, string(componentKindStandardFlow)) || sourcePackComponentKindEquals(component.Kind, "flow"):
			payload, payloadRef, err := readSourcePackComponentPayload(packRoot, component)
			if err != nil {
				r.addOfficialPackScanBlocker(sourcePackComponentKey(packName, component), "source_pack_flow_payload_unreadable", fmt.Sprintf("read standard_flow payload: %v", err), "Fix the source-backed Flow payload before using verify-zero as cutover evidence.")
				continue
			}
			r.appendSourcePackFlowDependencies(packName, component, payloadRef, payload, dependencies)
		case sourcePackComponentKindEquals(component.Kind, string(componentKindApproval)):
			payload, payloadRef, err := readSourcePackComponentPayload(packRoot, component)
			if err != nil {
				r.addOfficialPackScanBlocker(sourcePackComponentKey(packName, component), "source_pack_approval_payload_unreadable", fmt.Sprintf("read approval_process payload: %v", err), "Fix the source-backed approval payload before using verify-zero as cutover evidence.")
				continue
			}
			r.appendSourcePackApprovalDependencies(packName, component, payloadRef, payload, dependencies)
		}
	}
}

func sourcePackComponentKindEquals(kind componentKind, want string) bool {
	return strings.EqualFold(strings.TrimSpace(string(kind)), strings.TrimSpace(want))
}

func readSourcePackComponentPayload(packRoot string, component manifestComponent) ([]byte, string, error) {
	if len(component.Payload) > 0 {
		return append([]byte(nil), component.Payload...), "pack.json inline payload", nil
	}
	componentPath := strings.TrimSpace(component.Path)
	if componentPath == "" {
		return nil, "", fmt.Errorf("component path is required")
	}
	payloadPath := filepath.Join(packRoot, filepath.FromSlash(componentPath))
	payloadAbs, err := filepath.Abs(payloadPath)
	if err != nil {
		return nil, componentPath, err
	}
	packRootAbs, err := filepath.Abs(packRoot)
	if err != nil {
		return nil, componentPath, err
	}
	if !verifyZeroPathWithinDir(packRootAbs, payloadAbs) {
		return nil, componentPath, fmt.Errorf("component path %q escapes pack root", componentPath)
	}
	payload, err := os.ReadFile(payloadAbs)
	if err != nil {
		return nil, componentPath, err
	}
	return payload, filepath.ToSlash(componentPath), nil
}

func verifyZeroPathWithinDir(root, target string) bool {
	rel, err := filepath.Rel(root, target)
	if err != nil {
		return false
	}
	return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)))
}

func (r *Report) appendSourcePackWorkflowAlertDependency(packName string, component manifestComponent, dependencies *[]VerifyZeroDependency) {
	sourceKey := sourcePackComponentKey(packName, component)
	dependency := VerifyZeroDependency{
		Type:           "SourcePack.workflow_alert",
		SourceType:     sourceTypeOfficialPack,
		SourceKey:      sourceKey,
		Identifier:     strings.TrimSpace(component.Path),
		Owner:          packName,
		RuntimePath:    "official pack workflow_alert component -> _System_WorkflowAlert",
		Message:        "Official source-backed pack still declares a workflow_alert component. Reapplying this pack can recreate durable WorkflowAlert behavior after cutover.",
		Recommendation: "Remove the workflow_alert component from the official pack and represent notification behavior as Flow sendEmail metadata before rerunning verify-zero.",
	}
	*dependencies = append(*dependencies, dependency)
	r.addUnsupported(UnsupportedItem{
		Severity:       "blocker",
		SourceType:     dependency.SourceType,
		SourceKey:      dependency.SourceKey,
		Code:           "source_pack_workflow_alert_dependency",
		Field:          "kind",
		Value:          sourcePackWorkflowAlertKind,
		Message:        dependency.Message,
		Recommendation: dependency.Recommendation,
	})
}

func (r *Report) appendSourcePackFlowDependencies(packName string, component manifestComponent, payloadRef string, payload []byte, dependencies *[]VerifyZeroDependency) {
	var metadata models.FlowMetadata
	if err := json.Unmarshal(payload, &metadata); err != nil {
		r.addOfficialPackScanBlocker(sourcePackComponentKey(packName, component), "source_pack_flow_metadata_unparseable", fmt.Sprintf("parse source-backed Flow metadata %s: %v", payloadRef, err), "Fix the source-backed Flow payload before using verify-zero as cutover evidence.")
		return
	}
	for _, action := range metadata.ActionCalls {
		if !strings.EqualFold(strings.TrimSpace(action.ActionType), flowEmailAlertActionType) {
			continue
		}
		sourceKey := sourcePackActionSourceKey(sourcePackComponentKey(packName, component), action.Name)
		dependency := VerifyZeroDependency{
			Type:           "SourcePack.Flow.emailAlert",
			SourceType:     sourceTypeOfficialPack,
			SourceKey:      sourceKey,
			Identifier:     payloadRef,
			Owner:          packName,
			RuntimePath:    "official pack standard_flow -> Flow actionType=emailAlert -> WorkflowAlert compatibility dispatch",
			Message:        "Official source-backed Flow still declares an emailAlert action. Reapplying this pack can recreate legacy Flow emailAlert behavior after cutover.",
			Recommendation: "Replace this source-backed Flow actionCall with canonical sendEmail metadata before rerunning verify-zero.",
		}
		if strings.TrimSpace(action.ActionName) != "" {
			dependency.References = []string{constants.TableWorkflowAlert + ":" + strings.TrimSpace(action.ActionName)}
		}
		*dependencies = append(*dependencies, dependency)
		r.addUnsupported(UnsupportedItem{
			Severity:       "blocker",
			SourceType:     dependency.SourceType,
			SourceKey:      dependency.SourceKey,
			Code:           "source_pack_flow_email_alert_dependency",
			Field:          "actionType",
			Value:          flowEmailAlertActionType,
			Message:        dependency.Message,
			Recommendation: dependency.Recommendation,
		})
	}
}

func (r *Report) appendSourcePackApprovalDependencies(packName string, component manifestComponent, payloadRef string, payload []byte, dependencies *[]VerifyZeroDependency) {
	var metadata sourcePackApprovalProcessXML
	if err := xml.Unmarshal(payload, &metadata); err != nil {
		r.addOfficialPackScanBlocker(sourcePackComponentKey(packName, component), "source_pack_approval_process_unparseable", fmt.Sprintf("parse source-backed approval_process %s: %v", payloadRef, err), "Fix the source-backed approval_process payload before using verify-zero as cutover evidence.")
		return
	}
	if metadata.XMLName.Local != "ApprovalProcess" {
		r.addOfficialPackScanBlocker(sourcePackComponentKey(packName, component), "source_pack_approval_process_unparseable", fmt.Sprintf("parse source-backed approval_process %s: root element must be ApprovalProcess", payloadRef), "Fix the source-backed approval_process payload before using verify-zero as cutover evidence.")
		return
	}
	for _, action := range sourcePackApprovalActions(metadata) {
		if !approvalActionTypeNormalizesToAlert(action.Type) {
			continue
		}
		sourceKey := sourcePackActionSourceKey(sourcePackComponentKey(packName, component), sourcePackApprovalActionKey(action))
		dependency := VerifyZeroDependency{
			Type:           "SourcePack.ApprovalProcess.Alert",
			SourceType:     sourceTypeOfficialPack,
			SourceKey:      sourceKey,
			Identifier:     payloadRef,
			Owner:          packName,
			RuntimePath:    "official pack approval_process action type=alert -> ProcessAction.Alert -> WorkflowAlert compatibility dispatch",
			Message:        "Official source-backed approval_process still declares an alert action that normalizes to ProcessAction.Alert. Reapplying this pack can recreate legacy approval notification behavior after cutover.",
			Recommendation: "Replace this approval action with a Flow notification binding before rerunning verify-zero.",
		}
		if strings.TrimSpace(action.APIName) != "" {
			dependency.References = []string{constants.TableWorkflowAlert + ":" + strings.TrimSpace(action.APIName)}
		} else if strings.TrimSpace(action.EmailTemplateID) != "" {
			dependency.References = []string{constants.TableEmailTemplate + ":" + strings.TrimSpace(action.EmailTemplateID)}
		}
		*dependencies = append(*dependencies, dependency)
		r.addUnsupported(UnsupportedItem{
			Severity:       "blocker",
			SourceType:     dependency.SourceType,
			SourceKey:      dependency.SourceKey,
			Code:           "source_pack_approval_alert_dependency",
			Field:          "type",
			Value:          strings.TrimSpace(action.Type),
			Message:        dependency.Message,
			Recommendation: dependency.Recommendation,
		})
	}
}

func approvalActionTypeNormalizesToAlert(raw string) bool {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "alert", "emailalert", "email_alert", strings.ToLower(constants.ProcessActionTypeAlert):
		return true
	default:
		return false
	}
}

func sourcePackApprovalActions(metadata sourcePackApprovalProcessXML) []sourcePackApprovalActionFinding {
	var actions []sourcePackApprovalActionFinding
	appendGroup := func(group *sourcePackApprovalActionsXML, event, stepName string) {
		if group == nil {
			return
		}
		all := append([]sourcePackApprovalActionXML{}, group.Action...)
		all = append(all, group.Actions...)
		for order, action := range all {
			actions = append(actions, sourcePackApprovalActionFinding{
				Name:            strings.TrimSpace(action.Name),
				Type:            strings.TrimSpace(action.Type),
				EmailTemplateID: strings.TrimSpace(action.EmailTemplateID),
				APIName:         strings.TrimSpace(action.APIName),
				Event:           event,
				StepName:        strings.TrimSpace(stepName),
				Order:           order,
			})
		}
	}
	appendGroup(metadata.InitialSubmissionActions, constants.ApprovalActionEventSubmit, "")
	appendGroup(metadata.StepApprovalActions, constants.ApprovalActionEventStepApproval, "")
	appendGroup(metadata.FinalApprovalActions, constants.ApprovalActionEventFinalApproval, "")
	appendGroup(metadata.FinalRejectionActions, constants.ApprovalActionEventFinalRejection, "")
	appendGroup(metadata.RecallActions, constants.ApprovalActionEventRecall, "")
	appendGroup(metadata.ReassignActions, constants.ApprovalActionEventReassign, "")
	for _, step := range metadata.Steps {
		appendGroup(step.StepApprovalActions, constants.ApprovalActionEventStepApproval, step.Name)
		appendGroup(step.StepRejectionActions, constants.ApprovalActionEventStepRejection, step.Name)
	}
	return actions
}

func sourcePackApprovalActionKey(action sourcePackApprovalActionFinding) string {
	name := strings.TrimSpace(action.Name)
	if name == "" {
		name = fmt.Sprintf("#%d", action.Order+1)
	}
	return strings.Trim(strings.Trim(action.StepName+"/"+action.Event+"/"+name, "/"), "/")
}

func sourcePackComponentKey(packName string, component manifestComponent) string {
	key := strings.TrimSpace(component.BusinessKey)
	if key == "" {
		key = strings.TrimSpace(component.Path)
	}
	return strings.Trim(strings.TrimSpace(packName)+"/"+strings.TrimSpace(string(component.Kind))+"/"+key, "/")
}

func sourcePackActionSourceKey(componentKey, actionName string) string {
	actionName = strings.TrimSpace(actionName)
	if actionName == "" {
		return componentKey
	}
	return strings.Trim(componentKey+"/"+actionName, "/")
}

func officialPackSourceKey(rootAbs, manifestPath string) string {
	rel, err := filepath.Rel(rootAbs, manifestPath)
	if err != nil {
		return filepath.ToSlash(manifestPath)
	}
	return filepath.ToSlash(rel)
}

func (r *Report) addOfficialPackScanBlocker(sourceKey, code, message, recommendation string) {
	r.addUnsupported(UnsupportedItem{
		Severity:       "blocker",
		SourceType:     sourceTypeOfficialPack,
		SourceKey:      strings.TrimSpace(sourceKey),
		Code:           code,
		Message:        message,
		Recommendation: recommendation,
	})
}
