package organization

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/core/secrets"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/plan"
)

type organizationInviteRecord struct {
	ID                   string
	OrganizationID       string
	OrganizationName     string
	Email                string
	Role                 string
	TeamID               string
	TeamName             string
	DefaultWorkspaceRole string
	InviterUserID        string
	InviterName          string
	JoinPolicy           string
	SettingsStatus       string
	CreatedAt            time.Time
	ExpiresAt            time.Time
	AcceptedAt           *time.Time
	RevokedAt            *time.Time
}

type organizationMembershipRecord struct {
	MemberID     string
	MemberStatus string
	UserID       string
}

func (txs TxStore) ListOrganizationInvitesForUser(ctx context.Context, userID string, organizationID string) ([]OrganizationInviteView, error) {
	page, err := txs.ListOrganizationInvitesForUserPage(ctx, userID, organizationID, OrganizationInviteListOptions{Limit: MaxOrganizationInviteListLimit})
	if err != nil {
		return nil, err
	}
	return page.Invites, nil
}

func (txs TxStore) ListOrganizationInvitesForUserPage(ctx context.Context, userID string, organizationID string, options OrganizationInviteListOptions) (OrganizationInvitePage, error) {
	if _, err := txs.requireActiveOrganizationAdmin(ctx, txs.db, userID, organizationID); err != nil {
		return OrganizationInvitePage{}, err
	}
	limit := normalizeOrganizationInviteListLimit(options.Limit)
	offset, err := decodeOrganizationInviteListCursor(options.Cursor)
	if err != nil {
		return OrganizationInvitePage{}, err
	}

	rows, err := txs.db.Query(ctx, `
SELECT i.id,
       i.email,
       i.org_role,
       t.id,
       t.name,
       i.default_workspace_role,
       i.created_at,
       i.expires_at
FROM organization_invites i
JOIN organization_teams t ON t.id = i.default_team_id
WHERE i.organization_id = $1
  AND i.revoked_at IS NULL
  AND i.accepted_at IS NULL
  AND i.expires_at > NOW()
ORDER BY i.created_at DESC, i.id ASC
LIMIT $2 OFFSET $3
`, organizationID, limit+1, offset)
	if err != nil {
		return OrganizationInvitePage{}, fmt.Errorf("list organization invites: %w", err)
	}
	defer rows.Close()

	invites := make([]OrganizationInviteView, 0)
	for rows.Next() {
		var invite OrganizationInviteView
		if err := rows.Scan(
			&invite.ID,
			&invite.Email,
			&invite.Role,
			&invite.TeamID,
			&invite.TeamName,
			&invite.DefaultWorkspaceRole,
			&invite.CreatedAt,
			&invite.ExpiresAt,
		); err != nil {
			return OrganizationInvitePage{}, fmt.Errorf("scan organization invite: %w", err)
		}
		invites = append(invites, invite)
	}
	if err := rows.Err(); err != nil {
		return OrganizationInvitePage{}, fmt.Errorf("iterate organization invites: %w", err)
	}
	hasMore := len(invites) > limit
	if hasMore {
		invites = invites[:limit]
	}
	nextCursor := ""
	if hasMore {
		nextCursor = strconv.Itoa(offset + limit)
	}
	return OrganizationInvitePage{Invites: invites, HasMore: hasMore, NextCursor: nextCursor}, nil
}

func normalizeOrganizationInviteListLimit(limit int) int {
	if limit <= 0 {
		return DefaultOrganizationInviteListLimit
	}
	if limit > MaxOrganizationInviteListLimit {
		return MaxOrganizationInviteListLimit
	}
	return limit
}

func decodeOrganizationInviteListCursor(raw string) (int, error) {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return 0, nil
	}
	offset, err := strconv.Atoi(trimmed)
	if err != nil || offset < 0 {
		return 0, fmt.Errorf("cursor must be a non-negative integer")
	}
	return offset, nil
}

func (txs TxStore) CreateOrganizationInviteForUser(
	ctx context.Context,
	userID string,
	organizationID string,
	params CreateOrganizationInviteParams,
	tokenHash string,
	expiresAt time.Time,
) (OrganizationInviteView, error) {
	if _, err := txs.requireActiveOrganizationAdmin(ctx, txs.db, userID, organizationID); err != nil {
		return OrganizationInviteView{}, err
	}

	if err := txs.requireOrganizationInviteJoinPolicy(ctx, txs.db, organizationID); err != nil {
		return OrganizationInviteView{}, err
	}

	role, err := resolveOrganizationGovernedRole(params.Role, organizationRoleMember)
	if err != nil {
		return OrganizationInviteView{}, err
	}
	email := strings.ToLower(strings.TrimSpace(params.Email))
	if email == "" {
		return OrganizationInviteView{}, badRequest("email is required")
	}
	teamID, teamName, err := txs.resolveOrganizationTeam(ctx, txs.db, organizationID, params.TeamID)
	if err != nil {
		return OrganizationInviteView{}, err
	}
	team, err := txs.loadOrganizationTeam(ctx, txs.db, organizationID, teamID)
	if err != nil {
		return OrganizationInviteView{}, err
	}
	if team.Status != organizationTeamStatusActive {
		return OrganizationInviteView{}, badRequest("team is not active")
	}
	defaultWorkspaceRole, err := resolveOrganizationWorkspaceRole(params.DefaultWorkspaceRole, string(domain.WorkspaceRoleMember))
	if err != nil {
		return OrganizationInviteView{}, err
	}

	targetUser, err := txs.core.FindUserByEmail(ctx, email)
	switch {
	case err == nil:
		if _, membershipErr := txs.requireActiveOrganizationMembership(ctx, txs.db, targetUser.ID, organizationID); membershipErr == nil {
			return OrganizationInviteView{}, conflict("User already belongs to this organization.")
		} else if !errors.Is(membershipErr, ErrForbidden) {
			return OrganizationInviteView{}, membershipErr
		}
	case errors.Is(err, ErrNotFound):
	default:
		return OrganizationInviteView{}, err
	}

	inviteID, err := identifiers.NewEntityID("orgi")
	if err != nil {
		return OrganizationInviteView{}, fmt.Errorf("generate organization invite id: %w", err)
	}
	now := time.Now().UTC()
	if _, err := txs.db.Exec(ctx, `
INSERT INTO organization_invites (
    id,
    organization_id,
    email,
    org_role,
    default_team_id,
    default_workspace_role,
    token_hash,
    invited_by_user_id,
    created_at,
    expires_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
`, inviteID, organizationID, email, role, teamID, defaultWorkspaceRole, strings.TrimSpace(tokenHash), userID, now, expiresAt.UTC()); err != nil {
		if isUniqueViolation(err) {
			return OrganizationInviteView{}, conflict("Invite token already exists.")
		}
		return OrganizationInviteView{}, fmt.Errorf("insert organization invite: %w", err)
	}

	return OrganizationInviteView{
		ID:                   inviteID,
		Email:                email,
		Role:                 role,
		TeamID:               teamID,
		TeamName:             teamName,
		DefaultWorkspaceRole: defaultWorkspaceRole,
		CreatedAt:            now,
		ExpiresAt:            expiresAt.UTC(),
	}, nil
}

func (txs TxStore) RevokeOrganizationInviteForUser(ctx context.Context, userID string, organizationID string, inviteID string) error {
	if _, err := txs.requireActiveOrganizationAdmin(ctx, txs.db, userID, organizationID); err != nil {
		return err
	}

	commandTag, err := txs.db.Exec(ctx, `
UPDATE organization_invites
SET revoked_at = NOW()
WHERE organization_id = $1
  AND id = $2
  AND revoked_at IS NULL
  AND accepted_at IS NULL
`, organizationID, strings.TrimSpace(inviteID))
	if err != nil {
		return fmt.Errorf("revoke organization invite: %w", err)
	}
	if commandTag.RowsAffected() == 0 {
		return ErrNotFound
	}
	return nil
}

func (txs TxStore) FindOrganizationInviteInfoByToken(ctx context.Context, token string) (OrganizationInviteInfo, bool, error) {
	record, found, err := txs.loadOrganizationInviteByToken(ctx, txs.db, token, false)
	if err != nil || !found {
		return OrganizationInviteInfo{}, found, err
	}
	if err := validateOrganizationInviteRecord(record); err != nil {
		return OrganizationInviteInfo{}, true, err
	}
	return OrganizationInviteInfo{
		OrganizationID:       record.OrganizationID,
		OrganizationName:     record.OrganizationName,
		InviterName:          record.InviterName,
		InvitedEmail:         record.Email,
		Role:                 record.Role,
		TeamID:               record.TeamID,
		TeamName:             record.TeamName,
		DefaultWorkspaceRole: record.DefaultWorkspaceRole,
	}, true, nil
}

func (txs TxStore) HasPendingOrganizationInviteForEmail(ctx context.Context, email string) (bool, error) {
	row := txs.db.QueryRow(ctx, `
SELECT EXISTS (
  SELECT 1
  FROM organization_invites i
  JOIN organization_settings os ON os.organization_id = i.organization_id
  WHERE i.email = $1
    AND i.accepted_at IS NULL
    AND i.revoked_at IS NULL
    AND i.expires_at > NOW()
    AND os.status = 'active'
    AND os.join_policy = 'admin_invite'
)
`, strings.TrimSpace(email))
	var found bool
	if err := row.Scan(&found); err != nil {
		return false, fmt.Errorf("check pending organization invite: %w", err)
	}
	return found, nil
}

func (txs TxStore) AcceptOrganizationInviteByTokenIfExists(
	ctx context.Context,
	user model.User,
	token string,
) (OrganizationInviteAcceptance, bool, error) {
	return withTx2(ctx, txs.db, func(tx coredb.Tx) (OrganizationInviteAcceptance, bool, error) {
		invite, found, err := txs.loadOrganizationInviteByToken(ctx, tx, token, true)
		if err != nil || !found {
			return OrganizationInviteAcceptance{}, found, err
		}
		if err := validateOrganizationInviteRecord(invite); err != nil {
			return OrganizationInviteAcceptance{}, true, err
		}
		if invite.Email != user.Email {
			return OrganizationInviteAcceptance{}, true, ErrForbidden
		}

		team, err := txs.loadOrganizationTeam(ctx, tx, invite.OrganizationID, invite.TeamID)
		if err != nil {
			return OrganizationInviteAcceptance{}, true, err
		}
		if team.Status != organizationTeamStatusActive {
			return OrganizationInviteAcceptance{}, true, badRequest("invite team is not active")
		}

		existingMembership, membershipFound, err := txs.findOrganizationMembershipByUser(ctx, tx, invite.OrganizationID, user.ID)
		if err != nil {
			return OrganizationInviteAcceptance{}, true, err
		}

		now := time.Now().UTC()
		memberID := existingMembership.MemberID
		hasSeat, err := organizationHasActiveSeat(ctx, tx, invite.OrganizationID, user.ID)
		if err != nil {
			return OrganizationInviteAcceptance{}, true, err
		}
		if !hasSeat {
			if err := txs.checkQuotaInTx(ctx, tx, invite.OrganizationID, plan.MetricSeats, 1); err != nil {
				return OrganizationInviteAcceptance{}, true, err
			}
		}
		if !membershipFound {
			memberID, err = identifiers.NewEntityID("orgm")
			if err != nil {
				return OrganizationInviteAcceptance{}, true, fmt.Errorf("generate organization member id: %w", err)
			}
			defaultUnitID, _, err := txs.resolveOrganizationUnit(ctx, tx, invite.OrganizationID, "")
			if err != nil {
				return OrganizationInviteAcceptance{}, true, err
			}
			if _, err := tx.Exec(ctx, `
INSERT INTO organization_members (
    id,
    organization_id,
    user_id,
    primary_team_id,
    primary_unit_id,
    org_role,
    status,
    source,
    default_workspace_role,
    joined_at,
    invited_by_user_id,
    created_by_user_id,
    created_at,
    updated_at
) VALUES ($1, $2, $3, $4, $5, $6, 'active', 'invite', $7, $8, $9, $9, $8, $8)
`, memberID, invite.OrganizationID, user.ID, invite.TeamID, defaultUnitID, invite.Role, invite.DefaultWorkspaceRole, now, invite.InviterUserID); err != nil {
				if isUniqueViolation(err) {
					return OrganizationInviteAcceptance{}, true, conflict("User already belongs to this organization.")
				}
				return OrganizationInviteAcceptance{}, true, fmt.Errorf("insert organization member from invite: %w", err)
			}

			if err := txs.upsertOrganizationUnitMembership(ctx, tx, invite.OrganizationID, defaultUnitID, memberID, invite.InviterUserID, true); err != nil {
				return OrganizationInviteAcceptance{}, true, err
			}
			if err := txs.switchPrimaryUnitMembership(ctx, tx, invite.OrganizationID, memberID, defaultUnitID); err != nil {
				return OrganizationInviteAcceptance{}, true, err
			}
			if err := txs.reconcileOrganizationUnitBackedTeamMemberships(ctx, tx, invite.OrganizationID, defaultUnitID, invite.InviterUserID); err != nil {
				return OrganizationInviteAcceptance{}, true, err
			}
		} else {
			if existingMembership.MemberStatus == organizationMemberStatusActive {
				return OrganizationInviteAcceptance{}, true, conflict("User already belongs to this organization.")
			}
			if _, err := tx.Exec(ctx, `
UPDATE organization_members
SET org_role = $3,
    primary_team_id = $4,
    status = 'active',
    source = 'invite',
    default_workspace_role = $5,
    invited_by_user_id = $6,
    disabled_at = NULL,
    updated_at = $7
WHERE organization_id = $1 AND id = $2
`, invite.OrganizationID, memberID, invite.Role, invite.TeamID, invite.DefaultWorkspaceRole, invite.InviterUserID, now); err != nil {
				return OrganizationInviteAcceptance{}, true, fmt.Errorf("reactivate organization member from invite: %w", err)
			}
		}

		if err := txs.upsertOrganizationTeamMembership(ctx, tx, invite.OrganizationID, invite.TeamID, memberID, invite.InviterUserID, true); err != nil {
			return OrganizationInviteAcceptance{}, true, err
		}
		if err := txs.switchPrimaryTeamMembership(ctx, tx, invite.OrganizationID, memberID, invite.TeamID, invite.InviterUserID); err != nil {
			return OrganizationInviteAcceptance{}, true, err
		}
		if err := txs.ensureOrganizationDefaultTeamMembership(ctx, tx, invite.OrganizationID, memberID, invite.InviterUserID); err != nil {
			return OrganizationInviteAcceptance{}, true, err
		}

		if invite.AcceptedAt == nil {
			if _, err := tx.Exec(ctx, `
UPDATE organization_invites
SET accepted_at = $2
WHERE id = $1
`, invite.ID, now); err != nil {
				return OrganizationInviteAcceptance{}, true, fmt.Errorf("mark organization invite accepted: %w", err)
			}
		}

		workspaceIDs, err := txs.listAffectedWorkspacesForOrganizationMember(ctx, tx, invite.OrganizationID, memberID)
		if err != nil {
			return OrganizationInviteAcceptance{}, true, err
		}
		for _, workspaceID := range workspaceIDs {
			if _, _, _, err := txs.reconcileOrganizationMemberWorkspaceProjection(ctx, tx, invite.OrganizationID, workspaceID, memberID); err != nil {
				return OrganizationInviteAcceptance{}, true, err
			}
		}

		return OrganizationInviteAcceptance{
			OrganizationID:   invite.OrganizationID,
			OrganizationName: invite.OrganizationName,
			Role:             invite.Role,
		}, true, nil
	})
}

func (txs TxStore) UpdateOrganizationMemberRoleForUser(
	ctx context.Context,
	userID string,
	organizationID string,
	memberID string,
	requestedRole string,
) (OrganizationMemberView, error) {
	if _, err := txs.requireActiveOrganizationAdmin(ctx, txs.db, userID, organizationID); err != nil {
		return OrganizationMemberView{}, err
	}

	role, err := resolveOrganizationGovernedRole(requestedRole, "")
	if err != nil {
		return OrganizationMemberView{}, err
	}

	currentAccount, err := txs.loadOrganizationMemberView(ctx, txs.db, organizationID, memberID)
	if err != nil {
		return OrganizationMemberView{}, err
	}
	if currentAccount.SyncMode != "" && currentAccount.SyncMode != organizationSyncModeLocal {
		return OrganizationMemberView{}, badRequest("member is managed by provider sync")
	}
	if currentAccount.Role == organizationRoleOwner {
		return OrganizationMemberView{}, badRequest("organization owner role cannot be changed")
	}
	if currentAccount.Role == role {
		return currentAccount, nil
	}

	commandTag, err := txs.db.Exec(ctx, `
UPDATE organization_members
SET org_role = $3, updated_at = NOW()
WHERE organization_id = $1
  AND id = $2
  AND status = 'active'
`, organizationID, memberID, role)
	if err != nil {
		return OrganizationMemberView{}, fmt.Errorf("update organization member role: %w", err)
	}
	if commandTag.RowsAffected() == 0 {
		return OrganizationMemberView{}, ErrNotFound
	}
	return txs.loadOrganizationMemberView(ctx, txs.db, organizationID, memberID)
}

func (txs TxStore) requireOrganizationInviteJoinPolicy(ctx context.Context, queryer queryRower, organizationID string) error {
	row := queryer.QueryRow(ctx, `
SELECT join_policy
FROM organization_settings
WHERE organization_id = $1
`, organizationID)
	var joinPolicy string
	if err := row.Scan(&joinPolicy); err != nil {
		if coredb.IsNotFound(err) {
			return ErrNotFound
		}
		return fmt.Errorf("load organization join policy: %w", err)
	}
	if joinPolicy != "admin_invite" {
		return badRequest("organization does not accept admin invites")
	}
	return nil
}

func resolveOrganizationGovernedRole(requestedRole string, fallbackRole string) (string, error) {
	role := strings.TrimSpace(requestedRole)
	if role == "" {
		role = strings.TrimSpace(fallbackRole)
	}
	if role == "" {
		return "", badRequest("role is required")
	}
	switch role {
	case organizationRoleAdmin, organizationRoleMember:
		return role, nil
	default:
		return "", badRequest("role must be org_admin or org_member")
	}
}

func validateOrganizationInviteRecord(invite organizationInviteRecord) error {
	if invite.SettingsStatus != "active" {
		return badRequest("invite is no longer valid")
	}
	if invite.JoinPolicy != "admin_invite" {
		return badRequest("invite is no longer valid")
	}
	if invite.AcceptedAt != nil {
		return badRequest("invite already accepted")
	}
	if invite.RevokedAt != nil {
		return badRequest("invite revoked")
	}
	if time.Now().UTC().After(invite.ExpiresAt) {
		return badRequest("invite expired")
	}
	return nil
}

func (txs TxStore) loadOrganizationInviteByToken(
	ctx context.Context,
	queryer queryRower,
	token string,
	forUpdate bool,
) (organizationInviteRecord, bool, error) {
	tokenHash := secrets.HashToken(strings.TrimSpace(token))
	if tokenHash == "" {
		return organizationInviteRecord{}, false, nil
	}

	statement := `
SELECT i.id,
       i.organization_id,
       o.name,
       i.email,
       i.org_role,
       i.default_team_id,
       t.name,
       i.default_workspace_role,
       i.invited_by_user_id,
       COALESCE(NULLIF(inviter.display_name, ''), inviter.name),
       os.join_policy,
       os.status,
       i.created_at,
       i.expires_at,
       i.accepted_at,
       i.revoked_at
FROM organization_invites i
JOIN organizations o ON o.id = i.organization_id
JOIN organization_teams t ON t.id = i.default_team_id
JOIN organization_settings os ON os.organization_id = i.organization_id
JOIN users inviter ON inviter.id = i.invited_by_user_id
WHERE i.token_hash = $1
`
	if forUpdate {
		statement += ` FOR UPDATE`
	}

	row := queryer.QueryRow(ctx, statement, tokenHash)
	var invite organizationInviteRecord
	if err := row.Scan(
		&invite.ID,
		&invite.OrganizationID,
		&invite.OrganizationName,
		&invite.Email,
		&invite.Role,
		&invite.TeamID,
		&invite.TeamName,
		&invite.DefaultWorkspaceRole,
		&invite.InviterUserID,
		&invite.InviterName,
		&invite.JoinPolicy,
		&invite.SettingsStatus,
		&invite.CreatedAt,
		&invite.ExpiresAt,
		&invite.AcceptedAt,
		&invite.RevokedAt,
	); err != nil {
		if coredb.IsNotFound(err) {
			return organizationInviteRecord{}, false, nil
		}
		return organizationInviteRecord{}, false, fmt.Errorf("load organization invite: %w", err)
	}
	return invite, true, nil
}

func (txs TxStore) findOrganizationMembershipByUser(
	ctx context.Context,
	queryer queryRower,
	organizationID string,
	userID string,
) (organizationMembershipRecord, bool, error) {
	row := queryer.QueryRow(ctx, `
SELECT id, status, user_id
FROM organization_members
WHERE organization_id = $1
  AND user_id = $2
`, organizationID, userID)

	var membership organizationMembershipRecord
	if err := row.Scan(&membership.MemberID, &membership.MemberStatus, &membership.UserID); err != nil {
		if coredb.IsNotFound(err) {
			return organizationMembershipRecord{}, false, nil
		}
		return organizationMembershipRecord{}, false, fmt.Errorf("load organization membership by user: %w", err)
	}
	return membership, true, nil
}

func (txs TxStore) listAffectedWorkspacesForOrganizationMember(
	ctx context.Context,
	queryer queryer,
	organizationID string,
	memberID string,
) ([]string, error) {
	rows, err := queryer.Query(ctx, `
SELECT workspace_id
FROM (
    SELECT owag.workspace_id
    FROM organization_workspace_member_grants owag
    WHERE owag.organization_id = $1
      AND owag.organization_member_id = $2
      AND owag.status = 'active'
    UNION
    SELECT twg.workspace_id
    FROM organization_member_team_memberships tm
    JOIN organization_workspace_team_grants twg
      ON twg.organization_id = tm.organization_id
     AND twg.organization_team_id = tm.organization_team_id
    WHERE tm.organization_id = $1
      AND tm.organization_member_id = $2
      AND tm.status = 'active'
      AND twg.status = 'active'
) workspaces
ORDER BY workspace_id ASC
`, organizationID, memberID)
	if err != nil {
		return nil, fmt.Errorf("list affected workspaces for organization member: %w", err)
	}
	defer rows.Close()

	workspaceIDs := make([]string, 0)
	for rows.Next() {
		var workspaceID string
		if err := rows.Scan(&workspaceID); err != nil {
			return nil, fmt.Errorf("scan affected organization workspace: %w", err)
		}
		workspaceIDs = append(workspaceIDs, workspaceID)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate affected organization workspaces: %w", err)
	}
	return workspaceIDs, nil
}
