package keeper

import (
	errorsmod "cosmossdk.io/errors"

	sdk "git.cw.tr/mukan-network/mukan-sdk/types"

	"git.cw.tr/mukan-network/mukan-ibc/modules/core/02-client/types"
	"git.cw.tr/mukan-network/mukan-ibc/modules/core/exported"
	"git.cw.tr/mukan-network/mukan-ibc/modules/core/internal/telemetry"
)

// CreateClient generates a new client identifier and invokes the associated light client module in order to
// initialize a new client. An isolated prefixed store will be reserved for this client using the generated
// client identifier. The light client module is responsible for setting any client-specific data in the store
// via the Initialize method. This includes the client state, initial consensus state and any associated
// metadata. The generated client identifier will be returned if a client was successfully initialized.
func (k *Keeper) CreateClient(ctx sdk.Context, clientType string, clientState, consensusState []byte) (string, error) {
	if clientType == exported.Localhost {
		return "", errorsmod.Wrapf(types.ErrInvalidClientType, "cannot create client of type: %s", clientType)
	}

	clientID := k.GenerateClientIdentifier(ctx, clientType)

	clientModule, err := k.Route(ctx, clientID)
	if err != nil {
		return "", err
	}

	if err := clientModule.Initialize(ctx, clientID, clientState, consensusState); err != nil {
		return "", err
	}

	if status := clientModule.Status(ctx, clientID); status != exported.Active {
		return "", errorsmod.Wrapf(types.ErrClientNotActive, "cannot create client (%s) with status %s", clientID, status)
	}

	initialHeight := clientModule.LatestHeight(ctx, clientID)
	k.Logger(ctx).Info("client created at height", "client-id", clientID, "height", initialHeight.String())

	defer telemetry.ReportCreateClient(clientType)
	emitCreateClientEvent(ctx, clientID, clientType, initialHeight)

	return clientID, nil
}

// UpdateClient updates the consensus state and the state root from a provided header.
func (k *Keeper) UpdateClient(ctx sdk.Context, clientID string, clientMsg exported.ClientMessage) error {
	clientModule, err := k.Route(ctx, clientID)
	if err != nil {
		return err
	}

	if status := clientModule.Status(ctx, clientID); status != exported.Active {
		return errorsmod.Wrapf(types.ErrClientNotActive, "cannot update client (%s) with status %s", clientID, status)
	}

	if err := clientModule.VerifyClientMessage(ctx, clientID, clientMsg); err != nil {
		return err
	}

	foundMisbehaviour := clientModule.CheckForMisbehaviour(ctx, clientID, clientMsg)
	if foundMisbehaviour {
		clientModule.UpdateStateOnMisbehaviour(ctx, clientID, clientMsg)

		k.Logger(ctx).Info("client frozen due to misbehaviour", "client-id", clientID)

		clientType := types.MustParseClientIdentifier(clientID)
		defer telemetry.ReportUpdateClient(foundMisbehaviour, clientType, clientID)
		emitSubmitMisbehaviourEvent(ctx, clientID, clientType)

		return nil
	}

	consensusHeights := clientModule.UpdateState(ctx, clientID, clientMsg)

	k.Logger(ctx).Info("client state updated", "client-id", clientID, "heights", consensusHeights)

	clientType := types.MustParseClientIdentifier(clientID)
	defer telemetry.ReportUpdateClient(foundMisbehaviour, clientType, clientID)
	emitUpdateClientEvent(ctx, clientID, clientType, consensusHeights, k.cdc, clientMsg)

	return nil
}

// UpgradeClient upgrades the client to a new client state if this new client was committed to
// by the old client at the specified upgrade height
func (k *Keeper) UpgradeClient(
	ctx sdk.Context,
	clientID string,
	upgradedClient, upgradedConsState, upgradeClientProof, upgradeConsensusStateProof []byte,
) error {
	clientModule, err := k.Route(ctx, clientID)
	if err != nil {
		return err
	}

	if status := clientModule.Status(ctx, clientID); status != exported.Active {
		return errorsmod.Wrapf(types.ErrClientNotActive, "cannot upgrade client (%s) with status %s", clientID, status)
	}

	if err := clientModule.VerifyUpgradeAndUpdateState(ctx, clientID, upgradedClient, upgradedConsState, upgradeClientProof, upgradeConsensusStateProof); err != nil {
		return errorsmod.Wrapf(err, "cannot upgrade client with ID %s", clientID)
	}

	latestHeight := clientModule.LatestHeight(ctx, clientID)
	k.Logger(ctx).Info("client state upgraded", "client-id", clientID, "height", latestHeight.String())

	clientType := types.MustParseClientIdentifier(clientID)
	defer telemetry.ReportUpgradeClient(clientType, clientID)
	emitUpgradeClientEvent(ctx, clientID, clientType, latestHeight)

	return nil
}

// RecoverClient will invoke the light client module associated with the subject clientID requesting it to
// recover the subject client given a substitute client identifier. The light client implementation
// is responsible for validating the parameters of the substitute (ensuring they match the subject's parameters)
// as well as copying the necessary consensus states from the substitute to the subject client store.
// The substitute must be Active and the subject must not be Active.
func (k *Keeper) RecoverClient(ctx sdk.Context, subjectClientID, substituteClientID string) error {
	clientModule, err := k.Route(ctx, subjectClientID)
	if err != nil {
		return errorsmod.Wrap(types.ErrRouteNotFound, subjectClientID)
	}

	if status := clientModule.Status(ctx, subjectClientID); status == exported.Active {
		return errorsmod.Wrapf(types.ErrInvalidRecoveryClient, "cannot recover subject client (%s) with status %s", subjectClientID, status)
	}

	if status := clientModule.Status(ctx, substituteClientID); status != exported.Active {
		return errorsmod.Wrapf(types.ErrClientNotActive, "cannot recover client using substitute client (%s) with status %s", substituteClientID, status)
	}

	subjectLatestHeight := clientModule.LatestHeight(ctx, subjectClientID)
	substituteLatestHeight := clientModule.LatestHeight(ctx, substituteClientID)
	if subjectLatestHeight.GTE(substituteLatestHeight) {
		return errorsmod.Wrapf(types.ErrInvalidHeight, "subject client state latest height is greater or equal to substitute client state latest height (%s >= %s)", subjectLatestHeight, substituteLatestHeight)
	}

	if err := clientModule.RecoverClient(ctx, subjectClientID, substituteClientID); err != nil {
		return err
	}

	k.Logger(ctx).Info("client recovered", "client-id", subjectClientID)

	clientType := types.MustParseClientIdentifier(subjectClientID)
	defer telemetry.ReportRecoverClient(clientType, subjectClientID)
	emitRecoverClientEvent(ctx, subjectClientID, clientType)

	return nil
}
