/* SPDX-License-Identifier: GPL-2.0-only */

#include <bootstate.h>
#include <console/console.h>
#include <intelblocks/cse.h>
#include <intelblocks/pmc_ipc.h>
#include <security/vboot/vboot_common.h>
#include <soc/intel/common/reset.h>
#include <soc/pci_devs.h>
#include <timestamp.h>
#include <types.h>

#define PMC_IPC_MEI_DISABLE_ID			0xa9
#define PMC_IPC_MEI_DISABLE_SUBID_ENABLE	0
#define PMC_IPC_MEI_DISABLE_SUBID_DISABLE	1

enum cse_eop_result {
	CSE_EOP_RESULT_GLOBAL_RESET_REQUESTED,
	CSE_EOP_RESULT_SUCCESS,
	CSE_EOP_RESULT_ERROR,
	CSE_EOP_RESULT_DISABLED,
};

static bool cse_disable_mei_bus(void)
{
	struct bus_disable_message {
		uint8_t command;
		uint8_t reserved[3];
	} __packed msg = {
		.command = MEI_BUS_DISABLE_COMMAND,
	};
	struct bus_disable_resp {
		uint8_t command;
		uint8_t status;
		uint8_t reserved[2];
	} __packed reply = {};

	size_t reply_sz = sizeof(reply);

	if (!heci_send_receive(&msg, sizeof(msg), &reply, &reply_sz, HECI_MEI_ADDR)) {
		printk(BIOS_ERR, "HECI: Failed to Disable MEI bus\n");
		return false;
	}

	if (reply.status) {
		printk(BIOS_ERR, "HECI: MEI_Bus_Disable Failed (status: %d)\n", reply.status);
		return false;
	}

	return true;
}

static bool cse_disable_mei_devices(void)
{
	struct pmc_ipc_buffer req = { 0 };
	struct pmc_ipc_buffer rsp;
	uint32_t cmd;

	cmd = pmc_make_ipc_cmd(PMC_IPC_MEI_DISABLE_ID, PMC_IPC_MEI_DISABLE_SUBID_DISABLE, 0);
	if (pmc_send_ipc_cmd(cmd, &req, &rsp) != CB_SUCCESS) {
		printk(BIOS_ERR, "CSE: Failed to disable MEI devices\n");
		return false;
	}

	return true;
}

static enum cse_eop_result cse_send_eop(void)
{
	enum {
		EOP_REQUESTED_ACTION_CONTINUE = 0,
		EOP_REQUESTED_ACTION_GLOBAL_RESET = 1,
	};
	struct end_of_post_msg {
		struct mkhi_hdr hdr;
	} __packed msg = {
		.hdr = {
			.group_id = MKHI_GROUP_ID_GEN,
			.command = MKHI_END_OF_POST,
		},
	};
	struct end_of_post_resp {
		struct mkhi_hdr hdr;
		uint32_t requested_actions;
	} __packed resp = {};
	size_t resp_size = sizeof(resp);

	/* For a CSE-Lite SKU, if the CSE is running RO FW and the board is
	   running vboot in recovery mode, the CSE is expected to be in SOFT
	   TEMP DISABLE state. */
	if (CONFIG(SOC_INTEL_CSE_LITE_SKU) && vboot_recovery_mode_enabled() &&
	    cse_is_hfs1_com_soft_temp_disable()) {
		printk(BIOS_INFO, "HECI: coreboot in recovery mode; found CSE in expected SOFT "
		       "TEMP DISABLE state, skipping EOP\n");
		return CSE_EOP_RESULT_SUCCESS;
	}

	/*
	 * Prerequisites:
	 * 1) HFSTS1 CWS is Normal
	 * 2) HFSTS1 COM is Normal
	 * 3) Only sent after DID (accomplished by compiling this into ramstage)
	 */

	if (cse_is_hfs1_com_soft_temp_disable()) {
		printk(BIOS_ERR, "HECI: Prerequisites not met for sending EOP\n");
		if (CONFIG(SOC_INTEL_CSE_LITE_SKU))
			return CSE_EOP_RESULT_ERROR;
		return CSE_EOP_RESULT_DISABLED;
	}

	if (!cse_is_hfs1_cws_normal() || !cse_is_hfs1_com_normal()) {
		printk(BIOS_ERR, "HECI: Prerequisites not met for sending EOP\n");
		return CSE_EOP_RESULT_ERROR;
	}

	printk(BIOS_INFO, "HECI: Sending End-of-Post\n");

	if (!heci_send_receive(&msg, sizeof(msg), &resp, &resp_size, HECI_MKHI_ADDR)) {
		printk(BIOS_ERR, "HECI: EOP send/receive fail\n");
		return CSE_EOP_RESULT_ERROR;
	}

	if (resp.hdr.result) {
		printk(BIOS_ERR, "HECI: EOP Resp Failed: %u\n", resp.hdr.result);
		return CSE_EOP_RESULT_ERROR;
	}

	printk(BIOS_INFO, "CSE: EOP requested action: ");

	switch (resp.requested_actions) {
	case EOP_REQUESTED_ACTION_GLOBAL_RESET:
		printk(BIOS_INFO, "global reset\n");
		return CSE_EOP_RESULT_GLOBAL_RESET_REQUESTED;
	case EOP_REQUESTED_ACTION_CONTINUE:
		printk(BIOS_INFO, "continue boot\n");
		return CSE_EOP_RESULT_SUCCESS;
	default:
		printk(BIOS_INFO, "unknown %u\n", resp.requested_actions);
		return CSE_EOP_RESULT_ERROR;
	}
}

/*
 * On EOP error, the BIOS is required to send an MEI bus disable message to the
 * CSE, followed by disabling all MEI devices. After successfully completing
 * this, it is safe to boot.
 */
static void cse_handle_eop_error(void)
{
	if (!cse_disable_mei_bus())
		die("Failed to disable MEI bus while recovering from EOP error\n"
		    "Preventing system from booting into an insecure state.\n");

	if (!cse_disable_mei_devices())
		die("Error disabling MEI devices while recovering from EOP error\n"
		    "Preventing system from booting into an insecure state.\n");
}

static void handle_cse_eop_result(enum cse_eop_result result)
{
	switch (result) {
	case CSE_EOP_RESULT_GLOBAL_RESET_REQUESTED:
		printk(BIOS_INFO, "CSE requested global reset in EOP response, resetting...\n");
		do_global_reset();
		break;
	case CSE_EOP_RESULT_SUCCESS:
		printk(BIOS_INFO, "CSE EOP successful, continuing boot\n");
		break;
	case CSE_EOP_RESULT_DISABLED:
		printk(BIOS_INFO, "CSE is disabled, continuing boot\n");
		break;
	case CSE_EOP_RESULT_ERROR: /* fallthrough */
	default:
		printk(BIOS_ERR, "ERROR: Failed to send EOP to CSE, %d\n", result);
		/* For vboot, trigger recovery mode if applicable, as there is
		   likely something very broken in this case. */
		if (CONFIG(VBOOT) && !vboot_recovery_mode_enabled())
			cse_trigger_vboot_recovery(CSE_EOP_FAIL);

		/* In non-vboot builds or recovery mode, follow the BWG in order
		   to continue to boot securely. */
		cse_handle_eop_error();
		break;
	}
}

static void set_cse_end_of_post(void *unused)
{
	/*
	 * If CSE is already hidden then accessing CSE registers would be wrong and will
	 * receive junk, hence, return as CSE is already disabled.
	 */
	if (!is_cse_enabled()) {
		printk(BIOS_DEBUG, "CSE is disabled, cannot send End-of-Post (EOP) message\n");
		return;
	}

	set_cse_device_state(PCH_DEVFN_CSE, DEV_ACTIVE);

	timestamp_add_now(TS_ME_BEFORE_END_OF_POST);
	handle_cse_eop_result(cse_send_eop());
	timestamp_add_now(TS_ME_AFTER_END_OF_POST);

	set_cse_device_state(PCH_DEVFN_CSE, DEV_IDLE);
}

/*
 * Ideally, to give coreboot maximum flexibility, sending EOP would be done as
 * late possible. If HECI_DISABLE_USING_SMM is selected, then sending EOP must
 * be performed before the HECI bus is disabled, so these boards use
 * BS_PAYLOAD_LOAD, which happens before the HECI_DISABLE_USING_SMM Kconfig takes
 * effect (EOP is sent using the HECI bus).
 * Otherwise, EOP can be pushed a little later, and can be performed in
 * BS_PAYLOAD_BOOT instead.
 */
#if !CONFIG(HECI_DISABLE_USING_SMM)
BOOT_STATE_INIT_ENTRY(BS_PAYLOAD_BOOT, BS_ON_ENTRY, set_cse_end_of_post, NULL);
#else
BOOT_STATE_INIT_ENTRY(BS_PAYLOAD_LOAD, BS_ON_ENTRY, set_cse_end_of_post, NULL);
#endif
