package organization

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

type normalizedOrganizationSyncIdentity struct {
	externalProvider string
	externalID       string
	syncSource       string
	syncedAt         time.Time
}

type organizationDirectoryState struct {
	directoryBackend string
	syncMode         string
	revision         int64
}

func normalizeOrganizationSyncIdentity(params MarkOrganizationSyncedParams) (normalizedOrganizationSyncIdentity, error) {
	externalProvider := strings.TrimSpace(params.ExternalProvider)
	if externalProvider == "" {
		return normalizedOrganizationSyncIdentity{}, badRequest("externalProvider is required")
	}
	externalID := strings.TrimSpace(params.ExternalID)
	if externalID == "" {
		return normalizedOrganizationSyncIdentity{}, badRequest("externalId is required")
	}
	syncSource := strings.TrimSpace(params.SyncSource)
	if syncSource == "" {
		syncSource = organizationSyncSourceDefault
	}

	syncedAt := time.Now().UTC()
	if params.SyncedAt != nil {
		syncedAt = params.SyncedAt.UTC()
	}

	return normalizedOrganizationSyncIdentity{
		externalProvider: externalProvider,
		externalID:       externalID,
		syncSource:       syncSource,
		syncedAt:         syncedAt,
	}, nil
}

func (txs TxStore) loadOrganizationDirectoryState(ctx context.Context, queryer queryRower, organizationID string) (organizationDirectoryState, error) {
	row := queryer.QueryRow(ctx, `
SELECT directory_backend, directory_revision
FROM organization_settings
WHERE organization_id = $1
`, organizationID)

	var directoryBackend string
	var directoryRevision int64
	if err := row.Scan(&directoryBackend, &directoryRevision); err != nil {
		if coredb.IsNotFound(err) {
			return organizationDirectoryState{}, ErrNotFound
		}
		return organizationDirectoryState{}, fmt.Errorf("load organization directory backend: %w", err)
	}
	syncMode, err := resolveOrganizationSyncMode(directoryBackend)
	if err != nil {
		return organizationDirectoryState{}, err
	}
	return organizationDirectoryState{
		directoryBackend: directoryBackend,
		syncMode:         syncMode,
		revision:         directoryRevision,
	}, nil
}

func (txs TxStore) requireOrganizationLocalDirectoryWritesAllowed(ctx context.Context, queryer queryRower, organizationID string) error {
	state, err := txs.loadOrganizationDirectoryState(ctx, queryer, organizationID)
	if err != nil {
		return err
	}
	if state.syncMode == organizationSyncModeAuthority {
		return badRequest("organization directory is externally managed")
	}
	return nil
}

func (txs TxStore) requireOrganizationExternalDirectory(ctx context.Context, queryer queryRower, organizationID string) (organizationDirectoryState, error) {
	state, err := txs.loadOrganizationDirectoryState(ctx, queryer, organizationID)
	if err != nil {
		return organizationDirectoryState{}, err
	}
	if state.syncMode == organizationSyncModeLocal {
		return organizationDirectoryState{}, badRequest("organization directory backend is not external")
	}
	return state, nil
}

func validateExpectedDirectoryRevision(expected *int64, actual int64) error {
	if expected == nil {
		return nil
	}
	if *expected == actual {
		return nil
	}
	return conflict("organization directory backend revision changed")
}

// MarkOrganizationTeamSynced binds an existing org team to an external
// directory identity so later sync workers can treat it as provider-managed.
func (txs TxStore) MarkOrganizationTeamSynced(ctx context.Context, organizationID string, teamID string, params MarkOrganizationSyncedParams) (OrganizationTeamView, error) {
	identity, err := normalizeOrganizationSyncIdentity(params)
	if err != nil {
		return OrganizationTeamView{}, err
	}

	return withTxValue(ctx, txs.db, func(tx coredb.Tx) (OrganizationTeamView, error) {
		state, err := txs.requireOrganizationExternalDirectory(ctx, tx, organizationID)
		if err != nil {
			return OrganizationTeamView{}, err
		}
		if err := validateExpectedDirectoryRevision(params.ExpectedDirectoryRevision, state.revision); err != nil {
			return OrganizationTeamView{}, err
		}
		syncRun, err := txs.ensureOrganizationDirectorySyncRun(ctx, tx, organizationID, state, identity, params)
		if err != nil {
			return OrganizationTeamView{}, err
		}
		team, err := txs.loadOrganizationTeam(ctx, tx, organizationID, teamID)
		if err != nil {
			return OrganizationTeamView{}, err
		}
		if team.Status != organizationTeamStatusActive {
			return OrganizationTeamView{}, badRequest("team is not active")
		}

		if _, err := tx.Exec(ctx, `
UPDATE organization_teams
SET source = 'synced',
    external_provider = $3,
    external_id = $4,
    synced_at = $5,
    sync_mode = $6,
    sync_source = $7,
    last_sync_run_id = $8,
    updated_at = $9
WHERE organization_id = $1
  AND id = $2
`, organizationID, teamID, identity.externalProvider, identity.externalID, identity.syncedAt, state.syncMode, identity.syncSource, syncRun.ID, time.Now().UTC()); err != nil {
			if isUniqueViolation(err) {
				return OrganizationTeamView{}, conflict("External team identity already exists in organization.")
			}
			return OrganizationTeamView{}, fmt.Errorf("mark organization team synced: %w", err)
		}

		workspaces, err := txs.listGrantedWorkspacesForTeam(ctx, tx, organizationID, teamID)
		if err != nil {
			return OrganizationTeamView{}, err
		}
		memberIDs, err := txs.listActiveOrganizationMemberIDsForTeam(ctx, tx, organizationID, teamID)
		if err != nil {
			return OrganizationTeamView{}, err
		}
		for _, workspaceID := range workspaces {
			for _, memberID := range memberIDs {
				if _, _, _, err := txs.reconcileOrganizationMemberWorkspaceProjection(ctx, tx, organizationID, workspaceID, memberID); err != nil {
					return OrganizationTeamView{}, err
				}
			}
		}

		updatedTeam, err := txs.loadOrganizationTeam(ctx, tx, organizationID, teamID)
		if err != nil {
			return OrganizationTeamView{}, err
		}

		return organizationTeamViewFromRecord(updatedTeam), nil
	})
}

// MarkOrganizationUnitSynced binds an existing org unit to an external
// directory identity while keeping the unit model owned by agentteam-org.
func (txs TxStore) MarkOrganizationUnitSynced(ctx context.Context, organizationID string, unitID string, params MarkOrganizationSyncedParams) (OrganizationUnitView, error) {
	identity, err := normalizeOrganizationSyncIdentity(params)
	if err != nil {
		return OrganizationUnitView{}, err
	}

	return withTxValue(ctx, txs.db, func(tx coredb.Tx) (OrganizationUnitView, error) {
		state, err := txs.requireOrganizationExternalDirectory(ctx, tx, organizationID)
		if err != nil {
			return OrganizationUnitView{}, err
		}
		if err := validateExpectedDirectoryRevision(params.ExpectedDirectoryRevision, state.revision); err != nil {
			return OrganizationUnitView{}, err
		}
		syncRun, err := txs.ensureOrganizationDirectorySyncRun(ctx, tx, organizationID, state, identity, params)
		if err != nil {
			return OrganizationUnitView{}, err
		}
		unit, err := txs.loadOrganizationUnit(ctx, tx, organizationID, unitID)
		if err != nil {
			return OrganizationUnitView{}, err
		}
		if unit.Status != organizationUnitStatusActive {
			return OrganizationUnitView{}, badRequest("unit is not active")
		}

		if _, err := tx.Exec(ctx, `
UPDATE organization_units
SET external_provider = $3,
    external_id = $4,
    synced_at = $5,
    sync_mode = $6,
    sync_source = $7,
    last_sync_run_id = $8,
    updated_at = $9
WHERE organization_id = $1
  AND id = $2
`, organizationID, unitID, identity.externalProvider, identity.externalID, identity.syncedAt, state.syncMode, identity.syncSource, syncRun.ID, time.Now().UTC()); err != nil {
			if isUniqueViolation(err) {
				return OrganizationUnitView{}, conflict("External unit identity already exists in organization.")
			}
			return OrganizationUnitView{}, fmt.Errorf("mark organization unit synced: %w", err)
		}

		updatedUnit, err := txs.loadOrganizationUnitView(ctx, tx, organizationID, unitID)
		if err != nil {
			return OrganizationUnitView{}, err
		}

		return updatedUnit, nil
	})
}

// MarkOrganizationMemberSynced binds an existing organization member to an
// external directory identity without changing the workspace-first authz flow.
func (txs TxStore) MarkOrganizationMemberSynced(ctx context.Context, organizationID string, memberID string, params MarkOrganizationSyncedParams) (OrganizationMemberView, error) {
	identity, err := normalizeOrganizationSyncIdentity(params)
	if err != nil {
		return OrganizationMemberView{}, err
	}

	return withTxValue(ctx, txs.db, func(tx coredb.Tx) (OrganizationMemberView, error) {
		state, err := txs.requireOrganizationExternalDirectory(ctx, tx, organizationID)
		if err != nil {
			return OrganizationMemberView{}, err
		}
		if err := validateExpectedDirectoryRevision(params.ExpectedDirectoryRevision, state.revision); err != nil {
			return OrganizationMemberView{}, err
		}
		syncRun, err := txs.ensureOrganizationDirectorySyncRun(ctx, tx, organizationID, state, identity, params)
		if err != nil {
			return OrganizationMemberView{}, err
		}
		if _, err := txs.loadActiveOrganizationMember(ctx, tx, organizationID, memberID); err != nil {
			return OrganizationMemberView{}, err
		}

		if _, err := tx.Exec(ctx, `
UPDATE organization_members
SET source = 'sync',
    external_provider = $3,
    external_id = $4,
    synced_at = $5,
    sync_mode = $6,
    sync_source = $7,
    last_sync_run_id = $8,
    updated_at = $9
WHERE organization_id = $1
  AND id = $2
	`, organizationID, memberID, identity.externalProvider, identity.externalID, identity.syncedAt, state.syncMode, identity.syncSource, syncRun.ID, time.Now().UTC()); err != nil {
			if isUniqueViolation(err) {
				return OrganizationMemberView{}, conflict("External member identity already exists in organization.")
			}
			return OrganizationMemberView{}, fmt.Errorf("mark organization member synced: %w", err)
		}
		if err := txs.ensureOrganizationDefaultTeamMembershipForMember(ctx, tx, organizationID, memberID); err != nil {
			return OrganizationMemberView{}, err
		}
		if err := txs.reconcileOrganizationMemberAllWorkspaceProjections(ctx, tx, organizationID, memberID); err != nil {
			return OrganizationMemberView{}, err
		}

		updatedAccount, err := txs.loadOrganizationMemberView(ctx, tx, organizationID, memberID)
		if err != nil {
			return OrganizationMemberView{}, err
		}

		return updatedAccount, nil
	})
}
