package Thunderhorse::Router::Location;
$Thunderhorse::Router::Location::VERSION = '0.105';
use v5.40;
use Mooish::Base -standard;

use Gears::X::Thunderhorse;
use Thunderhorse qw(build_handler adapt_pagi);

extends 'Gears::Router::Location::SigilMatch';

has option 'action' => (
	isa => Str,
);

has field '_action_re' => (
	isa => RegexpRef,
	lazy => 1,
);

has param 'name' => (
	isa => Str,
	lazy => 1,
);

has param 'to' => (
	isa => Str | CodeRef,
	required => 0,
);

has param 'order' => (
	isa => Int,
	default => 0,
);

has param 'pagi' => (
	isa => Bool,
	default => false,
);

has param 'pagi_middleware' => (
	isa => CodeRef,
	required => 0,
);

has param 'controller' => (
	isa => InstanceOf ['Thunderhorse::Controller'],
);

has field 'pagi_app' => (
	isa => CodeRef,
	lazy => 1,
);

sub BUILD ($self, $)
{
	Gears::X::Thunderhorse->raise('controller has no method ' . $self->to)
		if defined $self->to && !$self->get_destination;

	# register the route in the router
	$self->router->_register_location($self->name, $self);
}

sub _build_action_re ($self)
{
	my ($scope, $method) = split /\./, $self->action;
	$scope = $scope eq '*' ? qr{[^.]+} : quotemeta lc $scope;

	if (($method // '*') eq '*') {
		$method = qr{(\.[^.]+)?};
	}
	elsif (lc $method eq 'get') {
		$method = '[.](get|head)';
	}
	else {
		$method = quotemeta lc ".$method";
	}

	return qr{^$scope$method$};
}

sub _build_name ($self)
{
	my $action = $self->action // 'any';
	my $pattern = $self->pattern;
	my $id = $self->router->_get_next_route_id;

	# autogenerated location name should be readable, but unique
	return "${id}_${action}_${pattern}";
}

sub _build_pagi_app ($self)
{
	# TODO: add router sealing when the app is started (prevent it from
	# changing and breaking assumptions)

	# this needs to be checked lazily, because we don't know if this is a
	# bridge in BUILD
	Gears::X::Thunderhorse->raise('PAGI apps cannot be bridges')
		if $self->pagi && $self->is_bridge;

	my $pagi = $self->pagi
		? adapt_pagi($self->get_destination)
		: build_handler($self->controller, $self->get_destination)
		;

	if (my $mw = $self->pagi_middleware) {
		$pagi = $mw->($pagi);
	}

	return $pagi;
}

sub get_destination ($self)
{
	my $to = $self->to;
	return undef unless defined $to;

	return $to if ref $to eq 'CODE';
	return $self->controller->can($self->to);
}

sub compare ($self, $path, $action)
{
	return undef if $self->has_action && $action !~ $self->_action_re;
	return $self->SUPER::compare($path);
}

__END__

=head1 NAME

Thunderhorse::Router::Location - Router location implementation

=head1 SYNOPSIS

	# locations are typically created through the router
	$router->add('/path/:id' => {
		to => 'handler',
		action => 'http.get',
		order => 10,
		pagi_middleware => sub ($app) { ... },
	});

=head1 DESCRIPTION

This class represents a single routing location in Thunderhorse. It extends
L<Gears::Router::Location::SigilMatch> to add Thunderhorse-specific
functionality like action filtering, PAGI app support, and middleware wrapping.
Locations are created by the router and handle matching URL patterns to
controller actions.

=head1 INTERFACE

Inherits all interface from L<Gears::Router::Location>, and adds the interface
documented below.

=head2 Attributes

=head3 action

Action pattern to match. Format is C<scope.method> where both parts can be
wildcards C<*>. If not passed, any action will match successfully.

I<Available in constructor>

=head3 name

The name of this location. Auto-generated if not provided, based on action and
pattern.

Name must be unique and deterministic. Names which are auto-generated are
always unique, but they are only deterministic if the locations are built in a
deterministic manner. Non-deterministic locations may cause problems when
caching with multiple workers.

I<Available in constructor>

=head3 to

The destination for this location. Can be a code reference or a string naming a
method in the controller. If not provided, the location is considered to be
unimplemented - nothing will get run if it gets matched, but
L</pagi_middleware> will still get executed. Having unimplemented bridges is
considered a valid use case for this behavior.

I<Available in constructor>

=head3 order

Integer controlling the order in which locations are matched. Lower numbers are
matched first. Default is C<0>.

I<Available in constructor>

=head3 pagi

Boolean indicating whether the C<to> destination is a native PAGI application.
Default is C<false>. Must be manually set to C<true> if the intent is to have a
PAGI application handling the route - there is currently no autodetection of
PAGI apps.

I<Available in constructor>

=head3 pagi_middleware

Code reference that wraps the location's PAGI app in middleware. The code ref
receives the PAGI app as an argument and should return a wrapped PAGI app.

I<Available in constructor>

=head3 controller

The controller instance in which context this location will be executed.

I<Available in constructor>

=head3 pagi_app

The built PAGI application for this location. Built lazily by combining the
destination handler with any middleware. This is the actual entry point that
gets called when the location matches.

=head2 Methods

=head3 new

	$object = $class->new(%args)

Standard Mooish constructor. Consult L</Attributes> section for available
constructor arguments.

=head3 get_destination

	$coderef = $object->get_destination()

Returns the destination code reference for this location. If C<to> is already a
code reference, returns it directly. If C<to> is a string, looks up the method
on the controller. Returns C<undef> if no destination is set.

=head1 SEE ALSO

L<Thunderhorse::Router>, L<Gears::Router::Location::SigilMatch>

