package setupcmd

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"os"
	"strings"

	mappingv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/layoutroutingidentitymappingv1"
	routingv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/layoutroutingsecurityv1"
	"github.com/pingcap-inc/nexuscrm-controlplane/catalogtenant"
)

const routingIdentityMappingPreflightCommand = "inspect-routing-identity-mapping"

// routingIdentityMappingPreflight is a deterministic, transient observation
// made while the migration environment is stopped. It is deliberately not an
// administrator authorization, mapping receipt, or durable migration state.
type routingIdentityMappingPreflight struct {
	Mode           string `json:"mode"`
	ProposalDigest string `json:"proposalDigest"`
	OrgID          string `json:"orgId"`
	// TenantNamespaceID records the active tenant-org binding observed for the
	// proposal OrgID. It does not claim that every ProfileKey or RoleKey in the
	// complete proposal originates from that tenant namespace.
	TenantNamespaceID   string `json:"tenantNamespaceId"`
	ActiveProfileCount  int    `json:"activeProfileCount"`
	ActiveUserRoleCount int    `json:"activeUserRoleCount"`
}

type routingIdentityMappingTargetInspector interface {
	InspectActiveOrgBindingLineage(context.Context, string) (catalogtenant.ObservedActiveOrgBinding, error)
}

type routingIdentityMappingIdentityReader interface {
	ActiveProfileIDs(context.Context) ([]string, error)
	ActiveUserRoleIDs(context.Context) ([]string, error)
}

type routingIdentityMappingSQLReader struct {
	db *sql.DB
}

func (reader routingIdentityMappingSQLReader) ActiveProfileIDs(ctx context.Context) ([]string, error) {
	return reader.activeIDs(
		ctx,
		"Profile",
		"SELECT `Id` FROM `Profile` WHERE COALESCE(`IsDeleted`, 0) = 0 ORDER BY BINARY `Id`",
	)
}

func (reader routingIdentityMappingSQLReader) ActiveUserRoleIDs(ctx context.Context) ([]string, error) {
	return reader.activeIDs(
		ctx,
		"UserRole",
		"SELECT `Id` FROM `UserRole` WHERE COALESCE(`IsDeleted`, 0) = 0 ORDER BY BINARY `Id`",
	)
}

func (reader routingIdentityMappingSQLReader) activeIDs(ctx context.Context, label, query string) ([]string, error) {
	if reader.db == nil {
		return nil, errors.New("routing identity mapping preflight: database is required")
	}
	rows, err := reader.db.QueryContext(ctx, query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var ids []string
	for rows.Next() {
		var id string
		if err := rows.Scan(&id); err != nil {
			return nil, err
		}
		if err := routingv1.ValidateResolvedLocalID(id); err != nil {
			return nil, fmt.Errorf("routing identity mapping preflight: malformed active %s ID", label)
		}
		ids = append(ids, id)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	return ids, nil
}

func inspectRoutingIdentityMapping(
	ctx context.Context,
	db *sql.DB,
	proposal mappingv1.Proposal,
) (routingIdentityMappingPreflight, error) {
	tenantAuthority, err := catalogtenant.New(db)
	if err != nil {
		return routingIdentityMappingPreflight{}, err
	}
	return inspectRoutingIdentityMappingFromSources(
		ctx,
		tenantAuthority,
		routingIdentityMappingSQLReader{db: db},
		proposal,
	)
}

func inspectRoutingIdentityMappingFromSources(
	ctx context.Context,
	targetInspector routingIdentityMappingTargetInspector,
	identityReader routingIdentityMappingIdentityReader,
	proposal mappingv1.Proposal,
) (routingIdentityMappingPreflight, error) {
	if targetInspector == nil || identityReader == nil {
		return routingIdentityMappingPreflight{}, errors.New("routing identity mapping preflight: source readers are required")
	}
	if err := mappingv1.ValidateProposal(proposal); err != nil {
		return routingIdentityMappingPreflight{}, err
	}

	target, err := targetInspector.InspectActiveOrgBindingLineage(ctx, proposal.OrgID)
	if err != nil {
		return routingIdentityMappingPreflight{}, err
	}
	if target.OrgID != proposal.OrgID || target.TenantNamespaceID == "" {
		return routingIdentityMappingPreflight{}, errors.New("routing identity mapping preflight: tenant target differs from proposal OrgID")
	}
	// The active tenant binding establishes the proposal's OrgID target. A
	// complete org mapping may also carry SYSTEM- or App-origin keys; this
	// read-only preflight cannot turn that source content into namespace
	// authorization. The later authenticated migration command owns that proof.

	profileIDs, err := identityReader.ActiveProfileIDs(ctx)
	if err != nil {
		return routingIdentityMappingPreflight{}, err
	}
	roleIDs, err := identityReader.ActiveUserRoleIDs(ctx)
	if err != nil {
		return routingIdentityMappingPreflight{}, err
	}
	if err := validateCompleteRoutingIdentityMapping(proposal, profileIDs, roleIDs); err != nil {
		return routingIdentityMappingPreflight{}, err
	}
	digest, err := mappingv1.ProposalDigest(proposal)
	if err != nil {
		return routingIdentityMappingPreflight{}, err
	}
	return routingIdentityMappingPreflight{
		Mode:                "halted_read_only_routing_identity_mapping_preflight/v1",
		ProposalDigest:      digest,
		OrgID:               proposal.OrgID,
		TenantNamespaceID:   target.TenantNamespaceID,
		ActiveProfileCount:  len(profileIDs),
		ActiveUserRoleCount: len(roleIDs),
	}, nil
}

func validateCompleteRoutingIdentityMapping(
	proposal mappingv1.Proposal,
	profileIDs, roleIDs []string,
) error {
	if err := mappingv1.ValidateProposal(proposal); err != nil {
		return err
	}
	if err := validateExactMappingDimension(
		proposal.Profiles,
		profileIDs,
		func(mapping mappingv1.ProfileMapping) string { return mapping.ResolvedProfileID },
		"Profile",
	); err != nil {
		return err
	}
	return validateExactMappingDimension(
		proposal.Roles,
		roleIDs,
		func(mapping mappingv1.RoleMapping) string { return mapping.ResolvedRoleID },
		"UserRole",
	)
}

func validateExactMappingDimension[T any](
	mappings []T,
	sourceIDs []string,
	resolvedID func(T) string,
	label string,
) error {
	source := make(map[string]struct{}, len(sourceIDs))
	for _, id := range sourceIDs {
		if err := routingv1.ValidateResolvedLocalID(id); err != nil {
			return fmt.Errorf("routing identity mapping preflight: malformed active %s ID", label)
		}
		if _, exists := source[id]; exists {
			return fmt.Errorf("routing identity mapping preflight: duplicate active %s ID", label)
		}
		source[id] = struct{}{}
	}
	if len(mappings) != len(source) {
		return fmt.Errorf("routing identity mapping preflight: %s mapping does not have exact active coverage", label)
	}
	mapped := make(map[string]struct{}, len(mappings))
	for _, mapping := range mappings {
		id := resolvedID(mapping)
		if _, exists := mapped[id]; exists {
			return fmt.Errorf("routing identity mapping preflight: duplicate %s mapping resolved ID", label)
		}
		mapped[id] = struct{}{}
		if _, exists := source[id]; !exists {
			return fmt.Errorf("routing identity mapping preflight: %s mapping does not resolve an active row", label)
		}
	}
	return nil
}

type routingIdentityMappingPreflightArgs struct {
	proposalFile string
}

func runRoutingIdentityMappingPreflightCommand(
	ctx context.Context,
	db *sql.DB,
	args []string,
	stdout io.Writer,
	stderr io.Writer,
	inspect func(context.Context, *sql.DB, mappingv1.Proposal) (routingIdentityMappingPreflight, error),
) int {
	parsed, err := parseRoutingIdentityMappingPreflightArgs(args)
	if err != nil {
		fmt.Fprintf(stderr, "%s: invalid arguments\n", routingIdentityMappingPreflightCommand)
		return 2
	}
	proposal, err := readCanonicalRoutingIdentityMappingProposal(parsed.proposalFile)
	if err != nil {
		fmt.Fprintf(stderr, "%s: invalid canonical proposal\n", routingIdentityMappingPreflightCommand)
		return 1
	}
	preflight, err := inspect(ctx, db, proposal)
	if err != nil {
		fmt.Fprintf(stderr, "%s: preflight failed\n", routingIdentityMappingPreflightCommand)
		return 1
	}
	if err := json.NewEncoder(stdout).Encode(preflight); err != nil {
		fmt.Fprintf(stderr, "%s: unable to emit observation\n", routingIdentityMappingPreflightCommand)
		return 1
	}
	return 0
}

func parseRoutingIdentityMappingPreflightArgs(args []string) (routingIdentityMappingPreflightArgs, error) {
	fs := flag.NewFlagSet(routingIdentityMappingPreflightCommand, flag.ContinueOnError)
	// The command has a no-target-local-ID output contract. Keep flag's raw
	// argument diagnostics out of stderr and let the caller emit one fixed
	// classification instead.
	fs.SetOutput(io.Discard)
	proposalFile := fs.String("proposal-file", "", "Path to one exact canonical LayoutRoutingIdentityMappingProposal binary")
	if err := fs.Parse(args); err != nil {
		return routingIdentityMappingPreflightArgs{}, err
	}
	if *proposalFile == "" {
		return routingIdentityMappingPreflightArgs{}, fmt.Errorf("%s: --proposal-file is required", routingIdentityMappingPreflightCommand)
	}
	if fs.NArg() != 0 {
		return routingIdentityMappingPreflightArgs{}, fmt.Errorf(
			"%s: unexpected arguments: %s",
			routingIdentityMappingPreflightCommand,
			strings.Join(fs.Args(), " "),
		)
	}
	return routingIdentityMappingPreflightArgs{proposalFile: *proposalFile}, nil
}

func readCanonicalRoutingIdentityMappingProposal(path string) (mappingv1.Proposal, error) {
	file, err := os.Open(path)
	if err != nil {
		return mappingv1.Proposal{}, err
	}
	defer file.Close()

	info, err := file.Stat()
	if err != nil {
		return mappingv1.Proposal{}, err
	}
	if !info.Mode().IsRegular() {
		return mappingv1.Proposal{}, errors.New("mapping proposal file must be regular")
	}
	if info.Size() < 1 || info.Size() > int64(mappingv1.MaxCanonicalProposalBytes) {
		return mappingv1.Proposal{}, fmt.Errorf(
			"mapping proposal file must be 1..%d bytes",
			mappingv1.MaxCanonicalProposalBytes,
		)
	}
	raw, err := io.ReadAll(io.LimitReader(file, int64(mappingv1.MaxCanonicalProposalBytes)+1))
	if err != nil {
		return mappingv1.Proposal{}, err
	}
	if len(raw) == 0 || len(raw) > mappingv1.MaxCanonicalProposalBytes {
		return mappingv1.Proposal{}, fmt.Errorf(
			"mapping proposal file must be 1..%d bytes",
			mappingv1.MaxCanonicalProposalBytes,
		)
	}
	return mappingv1.ParseCanonicalProposal(raw)
}
