<?php
/**
 * @package Hafenstudios\Kurato
 */

namespace Hafenstudios\Kurato\Rest;

use Hafenstudios\Kurato\Db\ContactRepository;
use Hafenstudios\Kurato\Db\MailEventRepository;
use Hafenstudios\Kurato\Db\StageRepository;
use Hafenstudios\Kurato\Db\SubscriptionRepository;

defined( 'ABSPATH' ) || exit;

/**
 * REST: Kontakte CRUD und Pipeline-Move (`kurato/v1/contacts`).
 *
 * Lesen erfordert hs_view_contacts (Persona Jonas), Aendern hs_manage_contacts.
 * Auch die Admin-UI (Kanban in K3) nutzt diese Endpunkte (D8).
 */
final class ContactsController extends AbstractController {

	public const REST_NAMESPACE = 'kurato/v1';

	private ContactRepository $contacts;

	private StageRepository $stages;

	private SubscriptionRepository $subscriptions;

	private ?MailEventRepository $events;

	/**
	 * @param ContactRepository        $contacts      Kontakt-Repository.
	 * @param StageRepository          $stages        Stufen-Repository.
	 * @param SubscriptionRepository   $subscriptions Abo-Repository.
	 * @param MailEventRepository|null $events        Ereignis-Log (optional, K4e-1;
	 *                                                 liefert das "seit"-Datum der
	 *                                                 Unzustellbarkeit).
	 */
	public function __construct( ContactRepository $contacts, StageRepository $stages, SubscriptionRepository $subscriptions, ?MailEventRepository $events = null ) {
		$this->contacts      = $contacts;
		$this->stages        = $stages;
		$this->subscriptions = $subscriptions;
		$this->events        = $events;
	}

	/**
	 * Registriert die Routen.
	 *
	 * @return void
	 */
	public function register_routes(): void {
		register_rest_route(
			self::REST_NAMESPACE,
			'/contacts',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'list_contacts' ),
					'permission_callback' => array( $this, 'can_view' ),
					'args'                => array(
						'page'     => array( 'type' => 'integer', 'default' => 1, 'minimum' => 1 ),
						'per_page' => array( 'type' => 'integer', 'default' => 25, 'minimum' => 1, 'maximum' => 100 ),
						'stage_id' => array( 'type' => 'integer', 'default' => 0 ),
						'search'   => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field' ),
					),
				),
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_contact' ),
					'permission_callback' => array( $this, 'can_manage' ),
				),
			)
		);

		register_rest_route(
			self::REST_NAMESPACE,
			'/contacts/(?P<id>\d+)',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_contact' ),
					'permission_callback' => array( $this, 'can_view' ),
				),
				array(
					'methods'             => \WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_contact' ),
					'permission_callback' => array( $this, 'can_manage' ),
				),
				array(
					'methods'             => \WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_contact' ),
					'permission_callback' => array( $this, 'can_manage' ),
				),
			)
		);

		register_rest_route(
			self::REST_NAMESPACE,
			'/contacts/(?P<id>\d+)/history',
			array(
				'methods'             => \WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_history' ),
				'permission_callback' => array( $this, 'can_view' ),
			)
		);

		// CP5: Notiz am Kontakt (Hans-Entwurf wird eine echte Notiz statt nur
		// eines Wiedervorlage-Titels). Aendernd, deshalb hs_manage_contacts.
		register_rest_route(
			self::REST_NAMESPACE,
			'/contacts/(?P<id>\d+)/notes',
			array(
				'methods'             => \WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'create_note' ),
				'permission_callback' => array( $this, 'can_manage' ),
				'args'                => array(
					'note' => array( 'type' => 'string', 'required' => true ),
				),
			)
		);

		register_rest_route(
			self::REST_NAMESPACE,
			'/contacts/(?P<id>\d+)/subscriptions',
			array(
				'methods'             => \WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_subscriptions' ),
				'permission_callback' => array( $this, 'can_view' ),
			)
		);

		register_rest_route(
			self::REST_NAMESPACE,
			'/contacts/(?P<id>\d+)/stage',
			array(
				'methods'             => \WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'move_stage' ),
				'permission_callback' => array( $this, 'can_manage' ),
				'args'                => array(
					'stage_id' => array( 'type' => array( 'integer', 'null' ), 'required' => true ),
				),
			)
		);
	}

	/**
	 * GET /contacts.
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response
	 */
	public function list_contacts( \WP_REST_Request $request ): \WP_REST_Response {
		$result = $this->contacts->query(
			array(
				'page'     => (int) $request->get_param( 'page' ),
				'per_page' => (int) $request->get_param( 'per_page' ),
				'stage_id' => (int) $request->get_param( 'stage_id' ),
				'search'   => (string) $request->get_param( 'search' ),
			)
		);

		// Antwort-Envelope wie bei den Kampagnen (items/total/page/per_page), damit
		// Konsumenten die Gesamtzahl und Seite ohne Header-Auswertung lesen koennen
		// (Live-Fund 23.07.: der Parser erwartete diese Struktur). Die WP-ueblichen
		// Pagination-Header bleiben zusaetzlich erhalten (Abwaertskompatibilitaet).
		$response = new \WP_REST_Response(
			array(
				'items'    => $result['items'],
				'total'    => (int) $result['total'],
				'page'     => (int) $result['page'],
				'per_page' => (int) $result['per_page'],
			),
			200
		);
		$response->header( 'X-WP-Total', (string) $result['total'] );
		$response->header( 'X-WP-TotalPages', (string) (int) ceil( $result['total'] / max( 1, $result['per_page'] ) ) );
		return $response;
	}

	/**
	 * POST /contacts.
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function create_contact( \WP_REST_Request $request ) {
		$data  = $this->read_input( $request );
		$has   = '' !== $data['email'] || '' !== $data['first_name'] || '' !== $data['last_name'] || '' !== $data['display_name'] || '' !== $data['phone'];
		if ( ! $has ) {
			return new \WP_Error(
				'kurato_empty_contact',
				__( 'Bitte geben Sie mindestens einen Namen oder eine E-Mail-Adresse an.', 'kurato' ),
				array( 'status' => 400 )
			);
		}

		// Neue Kontakte landen in der ersten Pipeline-Stufe.
		$stage_id           = $this->stages->first_stage_id();
		$data['stage_id']   = $stage_id > 0 ? $stage_id : null;
		$data['source']     = 'manual';

		$id = $this->contacts->insert( $data );
		if ( 0 === $id ) {
			return new \WP_Error(
				'kurato_create_failed',
				__( 'Der Kontakt konnte nicht angelegt werden. Bitte versuchen Sie es erneut.', 'kurato' ),
				array( 'status' => 500 )
			);
		}
		return new \WP_REST_Response( $this->contacts->get( $id ), 201 );
	}

	/**
	 * GET /contacts/{id}.
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function get_contact( \WP_REST_Request $request ) {
		$contact = $this->contacts->get( (int) $request->get_param( 'id' ) );
		if ( null === $contact ) {
			return $this->not_found();
		}
		return new \WP_REST_Response( $this->with_delivery_since( $contact ), 200 );
	}

	/**
	 * PUT /contacts/{id}.
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function update_contact( \WP_REST_Request $request ) {
		$id = (int) $request->get_param( 'id' );
		if ( null === $this->contacts->get( $id ) ) {
			return $this->not_found();
		}
		// Nur mitgesendete Felder aendern (Partial-Update): ein Teil-Update darf
		// nicht mitgesendete Felder nicht mit Leerwerten ueberschreiben.
		$this->contacts->update( $id, $this->read_patch( $request ) );
		return new \WP_REST_Response( $this->contacts->get( $id ), 200 );
	}

	/**
	 * DELETE /contacts/{id}.
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function delete_contact( \WP_REST_Request $request ) {
		$id = (int) $request->get_param( 'id' );
		if ( null === $this->contacts->get( $id ) ) {
			return $this->not_found();
		}
		$this->contacts->delete( $id );
		return new \WP_REST_Response( array( 'deleted' => true ), 200 );
	}

	/**
	 * GET /contacts/{id}/history - Stufen-Verlauf (Timeline).
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function get_history( \WP_REST_Request $request ) {
		$id = (int) $request->get_param( 'id' );
		if ( null === $this->contacts->get( $id ) ) {
			return $this->not_found();
		}

		// CP5: Stufenwechsel UND Notizen, neueste zuerst. Die Stufen-Zeilen
		// behalten ihre bisherigen Felder unveraendert (additiv kommen `type` und
		// `note` dazu), damit bestehende Konsumenten nichts merken.
		$rows = array();
		foreach ( $this->contacts->stage_history( $id ) as $step ) {
			$step['type'] = 'stage';
			$step['note'] = '';
			$rows[]       = $step;
		}
		foreach ( $this->contacts->notes( $id ) as $note ) {
			$rows[] = array(
				'id'         => 'n' . $note['id'], // kollidiert nie mit einer Stufen-ID
				'type'       => 'note',
				'note'       => $note['note'],
				'from_label' => '',
				'to_label'   => '',
				'changed_at' => $note['created_at'],
				'user_id'    => $note['user_id'],
			);
		}
		usort(
			$rows,
			static function ( array $a, array $b ): int {
				return strcmp( (string) $b['changed_at'], (string) $a['changed_at'] );
			}
		);

		return new \WP_REST_Response( $rows, 200 );
	}

	/**
	 * POST /contacts/{id}/notes - Notiz am Kontakt ablegen (CP5).
	 *
	 * Der Text wird bereinigt und auf 5000 Zeichen gekappt; die Antwort traegt
	 * die Notizen des Kontakts (neueste zuerst).
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function create_note( \WP_REST_Request $request ) {
		$id = (int) $request->get_param( 'id' );
		if ( null === $this->contacts->get( $id ) ) {
			return $this->not_found();
		}

		$note_id = $this->contacts->add_note( $id, (string) $request->get_param( 'note' ), get_current_user_id() );
		if ( 0 === $note_id ) {
			return new \WP_Error(
				'kurato_note_failed',
				__( 'Die Notiz konnte nicht gespeichert werden. Bitte geben Sie einen Text ein.', 'kurato' ),
				array( 'status' => 400 )
			);
		}

		return new \WP_REST_Response( array( 'notes' => $this->contacts->notes( $id ) ), 201 );
	}

	/**
	 * GET /contacts/{id}/subscriptions - Abos des Kontakts (mit Listen-Label).
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function get_subscriptions( \WP_REST_Request $request ) {
		$id = (int) $request->get_param( 'id' );
		if ( null === $this->contacts->get( $id ) ) {
			return $this->not_found();
		}
		return new \WP_REST_Response( $this->subscriptions->for_contact( $id ), 200 );
	}

	/**
	 * POST /contacts/{id}/stage - in eine Pipeline-Stufe verschieben.
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function move_stage( \WP_REST_Request $request ) {
		$id = (int) $request->get_param( 'id' );
		if ( null === $this->contacts->get( $id ) ) {
			return $this->not_found();
		}

		$raw      = $request->get_param( 'stage_id' );
		$stage_id = null === $raw ? null : absint( $raw );

		// Zielstufe muss existieren (null = aus der Pipeline nehmen ist erlaubt).
		if ( null !== $stage_id && null === $this->stages->get( $stage_id ) ) {
			return new \WP_Error(
				'kurato_stage_not_found',
				__( 'Die gewuenschte Stufe wurde nicht gefunden. Bitte laden Sie die Pipeline neu.', 'kurato' ),
				array( 'status' => 400 )
			);
		}

		$this->contacts->set_stage( $id, $stage_id, get_current_user_id() );
		return new \WP_REST_Response( $this->contacts->get( $id ), 200 );
	}

	/**
	 * Ergaenzt bei nicht-aktivem Zustellstatus das Datum des ausloesenden
	 * Ereignisses (`email_status_since`), damit das Kontaktprofil "unzustellbar
	 * seit ..." bzw. "Beschwerde eingegangen" mit Datum zeigen kann. Ist kein
	 * Ereignis-Log verdrahtet oder kein passendes Ereignis vorhanden, bleibt das
	 * Feld leer (das Profil zeigt den Status dann ohne Datum).
	 *
	 * @param array<string, mixed> $contact Kontaktzeile.
	 * @return array<string, mixed>
	 */
	private function with_delivery_since( array $contact ): array {
		$status = (string) ( $contact['email_status'] ?? 'active' );
		if ( 'active' === $status || null === $this->events ) {
			return $contact;
		}
		$types = 'suppressed_complaint' === $status ? array( 'complaint' ) : array( 'bounce_hard' );
		$event = $this->events->last_event_for_contact( (int) $contact['id'], $types );
		$contact['email_status_since'] = null !== $event ? (string) $event['created_at'] : '';
		return $contact;
	}

	/**
	 * Liest und bereinigt die Kontakt-Eingabefelder.
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return array<string, string>
	 */
	private function read_input( \WP_REST_Request $request ): array {
		$first   = sanitize_text_field( (string) $request->get_param( 'first_name' ) );
		$last    = sanitize_text_field( (string) $request->get_param( 'last_name' ) );
		$display = sanitize_text_field( (string) $request->get_param( 'display_name' ) );
		if ( '' === $display ) {
			$display = trim( $first . ' ' . $last );
		}
		return array(
			'email'        => sanitize_email( (string) $request->get_param( 'email' ) ),
			'first_name'   => $first,
			'last_name'    => $last,
			'display_name' => $display,
			'phone'        => sanitize_text_field( (string) $request->get_param( 'phone' ) ),
		);
	}

	/**
	 * Liest nur die tatsaechlich mitgesendeten Felder (fuer Partial-Updates).
	 *
	 * @param \WP_REST_Request $request Request.
	 * @return array<string, string>
	 */
	private function read_patch( \WP_REST_Request $request ): array {
		$patch = array();
		foreach ( array( 'email', 'first_name', 'last_name', 'display_name', 'phone' ) as $key ) {
			if ( ! $request->has_param( $key ) ) {
				continue;
			}
			$value         = (string) $request->get_param( $key );
			$patch[ $key ] = 'email' === $key ? sanitize_email( $value ) : sanitize_text_field( $value );
		}
		return $patch;
	}

	/**
	 * 404-Fehler.
	 *
	 * @return \WP_Error
	 */
	private function not_found() {
		return new \WP_Error(
			'kurato_contact_not_found',
			__( 'Der Kontakt wurde nicht gefunden. Bitte aktualisieren Sie die Liste.', 'kurato' ),
			array( 'status' => 404 )
		);
	}
}
