<?php
/**
 * /backreload/v1/notifiers
 *
 * @package Backreload\Api
 */

namespace Backreload\Api;

use Backreload\Notifiers\NotifierValidationException;
use WP_REST_Request;

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

class NotifiersController extends BaseController {

	public function register( string $namespace ): void {
		register_rest_route( $namespace, '/notifiers/catalog', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'catalog' ],
			'permission_callback' => [ $this, 'check_perm' ],
		] );

		register_rest_route( $namespace, '/notifiers', [
			[ 'methods' => 'GET',  'callback' => [ $this, 'index' ],  'permission_callback' => [ $this, 'check_perm' ] ],
			[ 'methods' => 'POST', 'callback' => [ $this, 'create' ], 'permission_callback' => [ $this, 'check_perm' ] ],
		] );

		register_rest_route( $namespace, '/notifiers/(?P<id>[a-zA-Z0-9_-]+)', [
			[ 'methods' => 'PATCH',  'callback' => [ $this, 'update' ],  'permission_callback' => [ $this, 'check_perm' ] ],
			[ 'methods' => 'DELETE', 'callback' => [ $this, 'destroy' ], 'permission_callback' => [ $this, 'check_perm' ] ],
		] );

		register_rest_route( $namespace, '/notifiers/(?P<id>[a-zA-Z0-9_-]+)/test', [
			'methods'             => 'POST',
			'callback'            => [ $this, 'test' ],
			'permission_callback' => [ $this, 'check_perm' ],
		] );
	}

	public function catalog() {
		return $this->ok( [ 'items' => $this->plugin->get( 'notifiers' )->catalog() ] );
	}

	public function index() {
		$mgr   = $this->plugin->get( 'notifiers' );
		$items = $mgr->configured();
		// Mask secrets, schema-driven so future notifiers don't have to register
		// their secret field names anywhere else — they just set `secret: true`
		// in their config_schema() entry.
		foreach ( $items as &$it ) {
			$handler = $mgr->has( $it['type'] ) ? $mgr->handler( $it['type'] ) : null;
			$secret_keys = [];
			if ( $handler ) {
				foreach ( $handler::config_schema() as $field ) {
					if ( ! empty( $field['secret'] ) ) {
						$secret_keys[ (string) $field['key'] ] = true;
					}
				}
			}
			foreach ( $it['config'] ?? [] as $k => $v ) {
				if ( $v && isset( $secret_keys[ $k ] ) ) {
					$it['config'][ $k ] = '***';
				}
			}
		}
		return $this->ok( [ 'items' => $items ] );
	}

	public function create( WP_REST_Request $req ) {
		$mgr = $this->plugin->get( 'notifiers' );
		$type   = (string) $req->get_param( 'type' );
		$name   = (string) ( $req->get_param( 'name' ) ?: '' );
		$config = (array)  ( $req->get_param( 'config' ) ?: [] );

		if ( ! $mgr->has( $type ) ) {
			return $this->error( 'bad_request', "Unknown notifier type: {$type}", 400 );
		}

		try {
			$config = $mgr->handler( $type )->validate( $config );
		} catch ( NotifierValidationException $e ) {
			return $this->error( 'validation_failed', 'Invalid configuration', 422, [ 'errors' => $e->errors() ] );
		}

		$items = $mgr->configured();
		$item = [
			'id'         => 'n_' . wp_generate_password( 8, false, false ),
			'type'       => $type,
			'name'       => $name ?: $mgr->handler( $type )::label(),
			'enabled'    => (bool) ( $req->get_param( 'enabled' ) ?? true ),
			'on_success' => (bool) ( $req->get_param( 'on_success' ) ?? false ),
			'on_failure' => (bool) ( $req->get_param( 'on_failure' ) ?? true ),
			'config'     => $config,
		];
		$items[] = $item;
		$mgr->save_configured( $items );

		return $this->ok( $item, 201 );
	}

	public function update( WP_REST_Request $req ) {
		$mgr   = $this->plugin->get( 'notifiers' );
		$items = $mgr->configured();
		$id    = (string) $req['id'];
		foreach ( $items as &$it ) {
			if ( $it['id'] !== $id ) continue;

			// Guard the handler lookup. In practice the type was validated at
			// create time, but a stored record with an unknown type (manual DB
			// edit, a notifier removed by a plugin downgrade) would otherwise
			// make handler() throw an unhandled exception on save. Mirror the
			// has()-guard that index() already uses.
			if ( ! $mgr->has( $it['type'] ) ) {
				return $this->error( 'unknown_type', "Unknown notifier type: {$it['type']}", 400 );
			}

			$bool_keys = [ 'enabled', 'on_success', 'on_failure' ];
			foreach ( [ 'name', 'enabled', 'on_success', 'on_failure' ] as $k ) {
				$v = $req->get_param( $k );
				if ( null === $v ) continue;
				$it[ $k ] = in_array( $k, $bool_keys, true ) ? (bool) $v : sanitize_text_field( (string) $v );
			}
			if ( null !== $req->get_param( 'config' ) ) {
				$new = array_merge( $it['config'] ?? [], (array) $req->get_param( 'config' ) );
				// Strip masked values so we don't overwrite real secrets with ***.
				//
				// Sourced from the notifier's own config_schema() — any field
				// whose schema entry has `secret: true` counts. Previously we
				// used a hard-coded name allow-list (`password`, `token`, …)
				// which silently failed for any future notifier that named its
				// secret something we hadn't anticipated (e.g. a `client_secret`
				// variant or a `mailgun_api_key`): the *** would flow into
				// validate() and overwrite the stored secret. Schema-driven
				// detection makes adding a new notifier safe by default.
				$handler     = $mgr->handler( $it['type'] );
				$secret_keys = [];
				foreach ( $handler::config_schema() as $field ) {
					if ( ! empty( $field['secret'] ) ) {
						$secret_keys[ (string) $field['key'] ] = true;
					}
				}
				foreach ( $new as $k => $v ) {
					if ( '***' === $v && isset( $secret_keys[ $k ] ) ) {
						$new[ $k ] = $it['config'][ $k ] ?? '';
					}
				}
				try {
					$it['config'] = $handler->validate( $new );
				} catch ( NotifierValidationException $e ) {
					return $this->error( 'validation_failed', 'Invalid configuration', 422, [ 'errors' => $e->errors() ] );
				}
			}
			$mgr->save_configured( $items );
			return $this->ok( $it );
		}
		return $this->error( 'not_found', 'Notifier not found', 404 );
	}

	public function destroy( WP_REST_Request $req ) {
		$mgr   = $this->plugin->get( 'notifiers' );
		$items = $mgr->configured();
		$id    = (string) $req['id'];
		$next = array_values( array_filter( $items, fn( $it ) => $it['id'] !== $id ) );
		if ( count( $next ) === count( $items ) ) {
			return $this->error( 'not_found', 'Notifier not found', 404 );
		}
		$mgr->save_configured( $next );
		return $this->ok( [ 'deleted' => true ] );
	}

	public function test( WP_REST_Request $req ) {
		$mgr   = $this->plugin->get( 'notifiers' );
		$items = $mgr->configured();
		$id    = (string) $req['id'];
		foreach ( $items as $it ) {
			if ( $it['id'] !== $id ) continue;
			try {
				$mgr->handler( $it['type'] )->send( $it['config'] ?? [], [
					'title'   => 'BackReload test',
					'message' => 'If you can read this, your notification channel is working ✅',
					'level'   => 'success',
					'context' => [ 'backup_id' => 'test', 'site_url' => home_url() ],
				] );
				return $this->ok( [ 'ok' => true ] );
			} catch ( \Throwable $e ) {
				return $this->error( 'test_failed', $e->getMessage(), 500 );
			}
		}
		return $this->error( 'not_found', 'Notifier not found', 404 );
	}

	/**
	 * Kept around for backward compatibility, but no longer called: both the
	 * index() mask path and the update() strip path now read `secret: true`
	 * off each notifier's config_schema() entry, which is the single source
	 * of truth. The legacy allow-list silently failed for any notifier whose
	 * secret field name wasn't on the list. Delete after one release cycle
	 * if no external code references it.
	 *
	 * @deprecated 0.2.0 Use config_schema()[*]['secret'] instead.
	 */
	private function is_secret_key( string $key ): bool {
		return in_array( strtolower( $key ), [ 'password','secret','secret_key','access_key','token','refresh_token','private_key','passphrase','webhook_url','bot_token','app_token','user_key' ], true );
	}
}
