<?php

/**
 * SkillsHandler — Domain handler for Lola's Skills actions.
 *
 * Owns: add_skill, activate_skill, deactivate_skill, update_skill,
 *       list_skills, delete_skill.
 *
 * @package LolaCore\Actions\Handlers
 * @license GPL-2.0-or-later
 */

declare( strict_types=1 );

namespace LolaCore\Actions\Handlers;

use LolaCore\Actions\ActionLogger;
use LolaCore\Actions\Contracts\ActionHandlerInterface;
use LolaCore\Skills\SkillDraftManager;
use LolaCore\Skills\YamlParser;
use LolaCore\Skills\SkillValidator;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class SkillsHandler implements ActionHandlerInterface {

	public function getSupportedActions(): array {
		return array(
			'add_skill',
			'activate_skill',
			'update_skill',
			'list_skills',
			'delete_skill',
			'deactivate_skill',
		);
	}

	public function handle( string $actionId, array $params, bool $dryRun ): array {
		return match ( $actionId ) {
			'add_skill'        => $this->addSkill( $params ),
			'activate_skill'   => $this->activateSkill( $params, $dryRun ),
			'update_skill'     => $this->updateSkill( $params, $dryRun ),
			'list_skills'      => $this->listSkills( $params ),
			'delete_skill'     => $this->deleteSkill( $params, $dryRun ),
			'deactivate_skill' => $this->deactivateSkill( $params, $dryRun ),
			default            => throw new \Exception( esc_html( "SkillsHandler: unknown action '{$actionId}'." ) ),
		};
	}

	// -----------------------------------------------------------------------
	// add_skill
	// -----------------------------------------------------------------------

	private function addSkill( array $params ): array {
		if ( ! current_user_can( 'manage_options' ) ) {
			throw new \Exception( 'Insufficient permissions to create skills.' );
		}

		$skillName = isset( $params['skill_name'] ) ? sanitize_title( (string) $params['skill_name'] ) : '';
		$content   = isset( $params['content'] ) ? (string) $params['content'] : '';

		if ( $skillName === '' ) {
			throw new \Exception( 'skill_name is required and must resolve to a valid kebab-case identifier.' );
		}
		if ( $content === '' ) {
			throw new \Exception( 'content is required.' );
		}

		// Parse + validate up-front so invalid content fails fast before any DB write.
		// We intentionally ignore the $dryRun flag here: this action is registered as
		// type=read in ActionExecutor, which means the chat loop hard-codes dryRun=true
		// for the auto-execute path. The skill being inserted as *inactive* (does not
		// fire until the user activates it) is already the safety mechanism — there
		// is no separate "preview without inserting" step in this flow.
		[ $frontmatterBlock, $body ] = $this->extractFrontmatter( $content );
		$parser      = new YamlParser();
		$validator   = new SkillValidator();
		$parsed      = $parser->parse( $frontmatterBlock );
		$validator->validate( $parsed );
		unset( $body );

		$draftManager = new SkillDraftManager( $parser, $validator );
		$id           = $draftManager->insert( $skillName, $content, 'lola_generated' );

		$logger = new ActionLogger();
		$logger->log(
			'add_skill',
			sprintf( 'Skill "%s" created in inactive state (id: %d).', $skillName, $id ),
			'success',
			'chat',
			array( 'skill_name' => $skillName, 'id' => $id )
		);

		return array(
			'success'    => true,
			'skill_name' => $skillName,
			'id'         => $id,
			'message'    => sprintf( 'Skill "%s" saved (inactive). Ask me to activate it when you are ready.', $skillName ),
		);
	}

	// -----------------------------------------------------------------------
	// activate_skill
	// -----------------------------------------------------------------------

	/**
	 * Switch a skill on so it starts influencing Lola's prompt.
	 *
	 * Registered as type=write in ActionExecutor, so the chat loop wraps this
	 * call in a confirmation card — the dry-run branch returns the preview
	 * the card needs to show, and the confirmed run performs the activation.
	 *
	 * Use this whether the skill is currently inactive (the user toggled it
	 * off, or it was just created by addSkill) or in any other non-active state:
	 * the unified verb is intentional, there is no separate "approve" flow.
	 */
	private function activateSkill( array $params, bool $dryRun ): array {
		if ( ! current_user_can( 'manage_options' ) ) {
			throw new \Exception( 'Insufficient permissions to activate skills.' );
		}

		$skillName = isset( $params['skill_name'] ) ? sanitize_title( (string) $params['skill_name'] ) : '';
		if ( $skillName === '' ) {
			throw new \Exception( 'skill_name is required.' );
		}

		$draftManager = new SkillDraftManager();
		$row          = $draftManager->getBySkillName( $skillName );
		if ( $row === null ) {
			throw new \Exception( sprintf( 'No skill found with name "%s".', esc_html( $skillName ) ) );
		}

		$id            = (int) $row['id'];
		$currentStatus = (string) ( $row['status'] ?? '' );

		if ( $currentStatus === 'active' ) {
			return array(
				'success'    => true,
				'skill_name' => $skillName,
				'message'    => sprintf( 'Skill "%s" is already active.', $skillName ),
			);
		}

		if ( $currentStatus === 'archived' ) {
			throw new \Exception( sprintf( 'Skill "%s" is archived and cannot be activated. Restore it from the admin panel first.', esc_html( $skillName ) ) );
		}

		if ( $dryRun ) {
			return array(
				'preview'    => true,
				'skill_name' => $skillName,
				'message'    => sprintf( 'Will activate skill "%s" (currently %s). After activation it will start influencing every reply matching its rules.', $skillName, $currentStatus ),
			);
		}

		$draftManager->activate( $id );

		$logger = new ActionLogger();
		$logger->log(
			'activate_skill',
			sprintf( 'Skill "%s" activated (id: %d).', $skillName, $id ),
			'success',
			'chat',
			array( 'skill_name' => $skillName, 'id' => $id )
		);

		return array(
			'success'    => true,
			'skill_name' => $skillName,
			'id'         => $id,
			'message'    => sprintf( 'Skill "%s" activated.', $skillName ),
		);
	}

	// -----------------------------------------------------------------------
	// update_skill
	// -----------------------------------------------------------------------

	/**
	 * Replace an existing skill's full markdown (frontmatter + body).
	 *
	 * Registered as type=write in ActionExecutor so the chat loop wraps this
	 * call in a confirmation card. Status is preserved: an active skill stays
	 * active after the update; an inactive one stays inactive.
	 */
	private function updateSkill( array $params, bool $dryRun ): array {
		if ( ! current_user_can( 'manage_options' ) ) {
			throw new \Exception( 'Insufficient permissions to update skills.' );
		}

		$skillName = isset( $params['skill_name'] ) ? sanitize_title( (string) $params['skill_name'] ) : '';
		$content   = isset( $params['content'] ) ? (string) $params['content'] : '';

		if ( $skillName === '' ) {
			throw new \Exception( 'skill_name is required.' );
		}
		if ( $content === '' ) {
			throw new \Exception( 'content is required.' );
		}

		// Parse + validate up-front so invalid YAML fails fast with a clear message.
		[ $frontmatterBlock ] = $this->extractFrontmatter( $content );
		$parser    = new YamlParser();
		$validator = new SkillValidator();
		$parsed    = $parser->parse( $frontmatterBlock );
		$validator->validate( $parsed );

		$draftManager = new SkillDraftManager( $parser, $validator );
		$row          = $draftManager->getBySkillName( $skillName );
		if ( $row === null ) {
			throw new \Exception( sprintf( 'No skill found with name "%s".', esc_html( $skillName ) ) );
		}

		$id     = (int) $row['id'];
		$status = (string) ( $row['status'] ?? '' );

		if ( $status === 'archived' ) {
			throw new \Exception( sprintf( 'Skill "%s" is archived and cannot be updated. Restore it first.', esc_html( $skillName ) ) );
		}

		if ( $dryRun ) {
			return array(
				'preview'    => true,
				'skill_name' => $skillName,
				'status'     => $status,
				'message'    => sprintf( 'Will replace the source of skill "%s" (currently %s). The status will be preserved.', $skillName, $status ),
			);
		}

		$draftManager->update( $id, $content );

		$logger = new ActionLogger();
		$logger->log(
			'update_skill',
			sprintf( 'Skill "%s" updated (id: %d, status preserved: %s).', $skillName, $id, $status ),
			'success',
			'chat',
			array( 'skill_name' => $skillName, 'id' => $id, 'status' => $status )
		);

		return array(
			'success'    => true,
			'skill_name' => $skillName,
			'id'         => $id,
			'status'     => $status,
			'message'    => sprintf( 'Skill "%s" updated.', $skillName ),
		);
	}

	// -----------------------------------------------------------------------
	// list_skills
	// -----------------------------------------------------------------------

	/**
	 * Return the real, current state of every skill in the system.
	 *
	 * Designed as Lola's ground-truth lookup: without this, the LLM tends to
	 * reconstruct skill state from session summaries (which decay) and
	 * confabulate phantom skills. Always call this before answering
	 * "which skills do I have?" — never trust memory for this.
	 *
	 * @param array $params Optional: 'status' = active|inactive|archived to filter.
	 */
	private function listSkills( array $params ): array {
		if ( ! current_user_can( 'manage_options' ) ) {
			throw new \Exception( 'Insufficient permissions to list skills.' );
		}

		$statusFilter = isset( $params['status'] ) ? sanitize_text_field( (string) $params['status'] ) : '';

		$loader   = new \LolaCore\Skills\SkillLoader( new YamlParser(), new SkillValidator() );
		$registry = new \LolaCore\Skills\SkillRegistry( $loader );
		$registry->load();

		$skills    = $registry->getAll();
		$bundled   = array( \LolaCore\Skills\Skill::ORIGIN_BUILT_IN, \LolaCore\Skills\Skill::ORIGIN_ADDON );
		$out       = array();
		$counts    = array( 'active' => 0, 'inactive' => 0, 'no-triggers' => 0, 'archived' => 0, 'other' => 0 );

		foreach ( $skills as $skill ) {
			$status = $skill->getStatus();
			$origin = $skill->getOrigin();

			if ( $statusFilter !== '' && $status !== $statusFilter ) {
				continue;
			}

			$bucket           = isset( $counts[ $status ] ) ? $status : 'other';
			$counts[ $bucket ] = $counts[ $bucket ] + 1;

			$out[] = array(
				'name'        => $skill->getName(),
				'description' => $skill->getDescription(),
				'status'      => $status,
				'origin'      => $origin,
				'priority'    => $skill->getPriority(),
				'manageable'  => ! in_array( $origin, $bundled, true ),
				'effective'   => $skill->isActivatable(),
			);
		}

		return array(
			'total'  => count( $out ),
			'counts' => $counts,
			'skills' => $out,
		);
	}

	// -----------------------------------------------------------------------
	// delete_skill
	// -----------------------------------------------------------------------

	/**
	 * Permanently remove a DB-origin skill row.
	 *
	 * Refuses to touch bundled (built-in / addon / filesystem) skills — those
	 * are managed by the master toggle or by the addon/plugin author, not by
	 * end users via chat.
	 */
	private function deleteSkill( array $params, bool $dryRun ): array {
		if ( ! current_user_can( 'manage_options' ) ) {
			throw new \Exception( 'Insufficient permissions to delete skills.' );
		}

		$skillName = isset( $params['skill_name'] ) ? sanitize_title( (string) $params['skill_name'] ) : '';
		if ( $skillName === '' ) {
			throw new \Exception( 'skill_name is required.' );
		}

		$draftManager = new SkillDraftManager();
		$row          = $draftManager->getBySkillName( $skillName );
		if ( $row === null ) {
			throw new \Exception( sprintf( 'No skill found with name "%s". (Bundled skills cannot be deleted from chat — only user/Lola-generated ones.)', esc_html( $skillName ) ) );
		}

		$id     = (int) $row['id'];
		$status = (string) ( $row['status'] ?? '' );

		if ( $dryRun ) {
			return array(
				'preview'    => true,
				'skill_name' => $skillName,
				'status'     => $status,
				'message'    => sprintf( 'Will permanently delete skill "%s" (currently %s). This cannot be undone.', $skillName, $status ),
			);
		}

		$draftManager->delete( $id );

		$logger = new ActionLogger();
		$logger->log(
			'delete_skill',
			sprintf( 'Skill "%s" deleted (id: %d, was %s).', $skillName, $id, $status ),
			'success',
			'chat',
			array( 'skill_name' => $skillName, 'id' => $id, 'previous_status' => $status )
		);

		return array(
			'success'    => true,
			'skill_name' => $skillName,
			'message'    => sprintf( 'Skill "%s" deleted.', $skillName ),
		);
	}

	// -----------------------------------------------------------------------
	// deactivate_skill
	// -----------------------------------------------------------------------

	/**
	 * Switch an active skill off so it stops firing.
	 *
	 * Preserves the content — exact counterpart of activateSkill. Use this
	 * when the user wants to pause a skill without losing its rules.
	 */
	private function deactivateSkill( array $params, bool $dryRun ): array {
		if ( ! current_user_can( 'manage_options' ) ) {
			throw new \Exception( 'Insufficient permissions to deactivate skills.' );
		}

		$skillName = isset( $params['skill_name'] ) ? sanitize_title( (string) $params['skill_name'] ) : '';
		if ( $skillName === '' ) {
			throw new \Exception( 'skill_name is required.' );
		}

		$draftManager = new SkillDraftManager();
		$row          = $draftManager->getBySkillName( $skillName );
		if ( $row === null ) {
			throw new \Exception( sprintf( 'No skill found with name "%s".', esc_html( $skillName ) ) );
		}

		$id     = (int) $row['id'];
		$status = (string) ( $row['status'] ?? '' );

		if ( $status !== 'active' ) {
			return array(
				'success'    => true,
				'skill_name' => $skillName,
				'status'     => $status,
				'message'    => sprintf( 'Skill "%s" is not active (current status: %s). Nothing to deactivate.', $skillName, $status ),
			);
		}

		if ( $dryRun ) {
			return array(
				'preview'    => true,
				'skill_name' => $skillName,
				'message'    => sprintf( 'Will move skill "%s" from active to inactive. It will stop firing but stay in the panel; you can reactivate it later.', $skillName ),
			);
		}

		$draftManager->deactivate( $id );

		$logger = new ActionLogger();
		$logger->log(
			'deactivate_skill',
			sprintf( 'Skill "%s" deactivated (id: %d, active → inactive).', $skillName, $id ),
			'success',
			'chat',
			array( 'skill_name' => $skillName, 'id' => $id )
		);

		return array(
			'success'    => true,
			'skill_name' => $skillName,
			'status'     => 'inactive',
			'message'    => sprintf( 'Skill "%s" deactivated. Now inactive.', $skillName ),
		);
	}

	// -----------------------------------------------------------------------
	// Internal helpers
	// -----------------------------------------------------------------------

	/**
	 * Extract YAML frontmatter block and body from full skill markdown.
	 *
	 * @return array{0: string, 1: string} [frontmatterBlock, body]
	 * @throws \InvalidArgumentException On missing delimiters.
	 */
	private function extractFrontmatter( string $content ): array {
		$lines    = preg_split( "/\r\n|\r|\n/", $content ) ?: array();
		$closeIdx = null;

		foreach ( $lines as $i => $line ) {
			if ( $i === 0 ) {
				if ( trim( $line ) !== '---' ) {
					throw new \InvalidArgumentException( 'Skill content is missing opening frontmatter delimiter "---".' );
				}
				continue;
			}
			if ( trim( $line ) === '---' ) {
				$closeIdx = $i;
				break;
			}
		}

		if ( $closeIdx === null ) {
			throw new \InvalidArgumentException( 'Skill content is missing closing frontmatter delimiter "---".' );
		}

		$frontmatterBlock = implode( "\n", array_slice( $lines, 1, $closeIdx - 1 ) );
		$body             = ltrim( implode( "\n", array_slice( $lines, $closeIdx + 1 ) ), "\r\n" );

		return array( $frontmatterBlock, $body );
	}
}
