package RapidApp::DirectLink::Link;

use strict;
use warnings;
use Moose;

use DateTime;
use RapidApp::JSON::MixedEncoder 'encode_json', 'decode_json';
use DateTime::Format::Flexible;
use Scalar::Util 'blessed';

=head1 NAME

RapidApp::DirectLink::Link

=head1 SYNOPSIS

  # generating a link for the user
  my $linkParams= {
    auth => { user => 5, acl => $aclForResource, },
    targetUrl => 'approot/externalUserRecap',
    stash_params => {foo=>1, bar=>2},
    sess_params => {SpecialSetting => 'blah blah'},
  };
  
  my $link= RapidApp::DirectLink::Link->new($linkParams);
  $self->c->model('DB')->createDirectLink($link);
  - or -
  my $link= $self->c->model('DB')->createDirectLink($linkParams);
  print $self->c->namespace."/DirectLink?id=".$link->linkUid;
  
  # storing a link to the database
  $self->c->model("DB::direct_link")->create({
    createDate => $link->creationDate,
    randomHash => $link->randomHash,
    params => $link->params,
  });
  
  # retrieving a link from the database
  my $uid= $self->c->request->params->{id};
  my $link= RapidApp::DirectLink::Link->new(linkUid => $uid); # parse the id string into a date and hash
  my $row= $self->c->model("DB::direct_link")->find( # get the other parameters from the database
    createDate => $link->createDate,
    randomHash => $link->randomhash
  );
  $link->contactId($row->contact_id); # set the contact_id
  $link->paramsJson($row->params);    # populate the rest of the params by applying a string of JSON

=cut

has 'creationDate'  => ( is => 'rw', isa => 'DateTime', lazy_build => 1, trigger => \&fixTz );
has 'randomHash'    => ( is => 'rw', isa => 'Str', lazy_build => 1 );

has 'target'        => ( is => 'rw', isa => 'Str|ArrayRef[Str]', required => 1 );
sub isRedirect { return !ref ((shift)->target); }
sub targetUrl {
	my $self= shift;
	$self->isRedirect or die 'target is not a URL';
	return $self->target;
}
sub isAction   { return ref ((shift)->target) eq 'ARRAY'; }
sub targetAction {
	my $self= shift;
	$self->isAction or die 'target is not an action';
	return $self->target;
}

has 'auth'          => ( is => 'rw', isa => 'HashRef', required => 1 );
has 'requestParams' => ( is => 'rw' ); # isa HashRef, but allowed to be undef
has 'session'       => ( is => 'rw' ); # isa HashRef, but allowed to be undef
has 'stash'         => ( is => 'rw' ); # isa HashRef, but allowed to be undef

has 'handlerUrl'    => ( is => 'rw' ); # the external URL to follow for this link, populated by Linkfactory when applicable

=head1 ATTRIBUTES

=over

=item createDate

The date the DirectLink was created.  This becomes part of the external ID string, increasing the
complexity of the id to be 36^8 per calendar day.  It is separated out into its own field, but
part of the primary key to permit sorting the DirectLink rows by date without needing a second
index on this fast-growing frequently-written table.

=item randomHash

The hash is simply a random number that uniquely (when combined with createDate) identifies this
record.  The hash is an ascii string legal for URLs, and currently 8 characters long
(generated by LinkFactory, see that module for details)

=item auth

Auth is a hash of credentials or permissions which are granted to anyone posessing the linkUid.
Typical keys for this object are a username or ID, and some sort of permissions list, or
allowed roles.

These details are used by DirectLink::SessionLoader, and can be overrridden to handle whatever
behavior or security the local application requires.

=item targetUrl

The targetUrl is the module path to which the user will be directed when they log in using
this link.  See DirectLink::SessionLoader

=item requestParams

req_params is the set of parameters which will be passed in the first request to the the
module specified in targetUrl.  This can be any hash of strings valid for an HTTP request.
**HOWEVER**, be sure to keep this hash **SMALL** in order to reduce the space needed by
DirectLink table records.  If you need lots of parameters, try just putting the keys here
and pulling the rest from appropriate tables when the user logs in.

req_params is mainly intended for reusing an already-written module.  If you write a custom
module to handle a DirectLink, consider stash_params instead.

=item session

sess_params is the set of parameters which will be put into the user's session when they
follow this link.  As usual, try to avoid using the session when the request will suffice.
However, always put security-sensitive values here (i.e. anything you don't want the user
to be able to alter).

=item stash

stash_params is the set of temporary initial parameters which will be put into the "stash",
where either a controller or view or Module will make use of them to build the initial UI
for the user.  Note that if you use the "redirectToLink" method in your controller, you will
be unable to make use of the stash, as it will vaporize before the next request.
(You could, however, save the stash in the session until the next request, but that gets hachish)

The intended use case is
  User: request with id=2lkj3h5423ljh4 => controller/DirectLink
  DirectLink.pm: create user session, or append to existing
  DirectLink.pm: convert id to stash_params
  DirectLink.pm: pass to TopController->Controller(targetUrl)
  Some/Module.pm: use stash params to build UI.

=item linkUid

See METHODS

=item params

See METHODS

=back

=head1 METHODS

=cut

# force all dates to be UTC time.  If it is floating, assume local time first
sub fixTz {
	my ($self, $date, $oldDate)= @_;
	$date->set_time_zone('local') if $date->time_zone eq 'floating';
	$date->set_time_zone('UTC');
	$date->set_hour(0);
	$date->set_minute(0);
	$date->set_second(0);
}

my $dateParser= DateTime::Format::Flexible->new;
my $coerceDatetime= sub {
	my $arg= shift;
	
	blessed $arg and return $arg;
	my $ref= ref $arg;
	$ref eq 'HASH' and return DateTime->new($arg);
	$ref eq ''     and return $dateParser->parse_datetime($arg);
	die "Cannot convert $ref to DateTime";
};

around BUILDARGS => sub {
	my $orig= shift;
	my $class= shift;
	my $params= (ref $_[0] eq 'HASH')? $_[0] : { @_ };
	
	# linkUid is a virtual property.  Convert it to creationDate and randomHash
	if (defined $params->{linkUid}) {
		my $uid= delete $params->{linkUid};
		$params->{randomHash}= $class->hashFromLinkUid($uid);
		$params->{creationDate}= $class->dateFromLinkUid($uid);
	}
	
	# convert creationDate to a DateTime object if needed
	defined $params->{creationDate} and $params->{creationDate}= $coerceDatetime->($params->{creationDate});
	
	# if 'params' is given, split it out
	if (defined $params->{params}) {
		my $p= $params->{params};
		ref $p eq 'HASH' or $p= RapidApp::JSON::MixedEncoder::decode_json($p);
		$params->{auth}=          $p->{auth}  if defined $p->{auth};
		$params->{target}=        $p->{url}   if defined $p->{url};
		$params->{requestParams}= $p->{req}   if defined $p->{req};
		$params->{session}=       $p->{sess}  if defined $p->{sess};
		$params->{stash}=         $p->{stash} if defined $p->{stash};
	}
	
	return $class->$orig($params);
};

sub _build_randomHash {
	;
}

sub _build_creationDate {
	die "creationDate must be set, or generated by the LinkFactory before using it";
}

=head2 linkUid

Calling linkUid with no parameters joins together createDate and randomHash into a UID
to be used in DirectLink urls.

Calling this method with a string parameter will parse the string into those two fields,
or die trying.

=cut

sub linkUid {
	my $self= shift;
	if (scalar(@_)) {
		scalar(@_) == 1 && ref $_[0] eq '' or die "invalid argument";
		my $uid= $_[0];
		length($uid) == 14 or die "Link UIDs must be 14 characters long";
		$self->randomHash($self->hashFromLinkUid($uid));
		$self->createDate($self->dateFromLinkUid($uid));
		return $uid;
	} else {
		my $d= $self->creationDate;
		return sprintf('%06x%s', ($d->year*15+$d->month)*40+$d->day, $self->randomHash);
	}
}

=head2 linkUrl

Returns handlerUrl with a id parameter of linkUid

=cut

sub linkUrl {
	my $self= shift;
	return sprintf('%s?id=%s', $self->handlerUrl, $self->linkUid);
}

=head2 dateFromLinkUid

This is a package-method (can also be an instance method) which returns a DateTime object
representing the encoded date portion of a UID.

=cut

sub dateFromLinkUid {
	my ($ignored, $uid)= @_;
	length($uid) == 14 or die "Link UIDs must be 14 characters long";
	my $day= hex(substr($uid,0,6));
	{use integer;
		my $month= $day / 40; # leave some space, for error checking
		my $year= $month / 15;
		return DateTime->new(year=>$year, month=>$month%15, day=>$day%40);
	}
}
sub hashFromLinkUid {
	my ($ignored, $uid)= @_;
	length($uid) == 14 or die "Link UIDs must be 14 characters long";
	return substr($uid,6);
}

=head2 params

Calling params with no parameters will build and return a hash appropriate for serializing into
the database.

Passing a hash to this function will overwrite any attributes which are part of 'params'
(even clearing attributes which aren't set in the hash).

=cut

sub params {
	my $self= shift;
	if (scalar(@_)) {
		my $params= (ref $_[0] eq 'HASH')? $_[0] : { @_ };
		defined $params->{url} or die "Params must contain a 'url' key, for the target field";
		$self->target($params->{url});
		$self->auth($params->{auth});
		$self->requestParams($params->{req});
		$self->session($params->{sess});
		$self->stash($params->{stash});
		return $params;
	}
	return {
		url => $self->target,
		auth => $self->auth,
		req => $self->requestParams,
		sess => $self->session,
		stash => $self->stash,
	};
}

=head2 paramsJson

This function is a convenience method to get or set params as a JSON string.

=cut

sub paramsJson {
	my ($self, $param)= @_;
	if (defined $param) {
		$self->params(RapidApp::JSON::MixedEncoder::decode_json($param));
		return $param;
	}
	else {
		return RapidApp::JSON::MixedEncoder::encode_json $self->params;
	}
}

=head1 SEE ALSO

RapidApp::DirectLink::LinkFactory

RapidApp::DirectLink::Redirector

=cut

1;
