# Copyright © 2010 Julian Blake Kongslie <jblake@omgwallhack.org>
# Licensed under the BSD 3-clause license.

use strict;
use warnings;

package Piny::User;

use Moose;
use Moose::Util::TypeConstraints;
use MooseX::StrictConstructor;

use Piny::Config;
use Piny::Email;
use Piny::Group;

# Types

subtype 'Username'
  => as 'Str'
  => where { $_ =~ /^(?!(git|iki)-)[a-zA-Z][a-zA-Z0-9_.-]{0,30}$/ }
  => message { if ( /^((?:git|iki)-|[^a-zA-Z])/ ) { "Usernames are not allowed to begin with $1" } elsif ( /([^a-zA-Z0-9_.-])/ ) { "Usernames are not allowed to contain $1" } elsif ( /[a-zA-Z0-9_.-]{32,}/ ) { "Usernames are not allowed to be more than 31 bytes" } else { "Invalid username" } }
  ;

# Attributes

has 'uid' =>
  ( is          => 'ro'
  , isa         => 'Int'
  , lazy_build  => 1
  );

has 'name' =>
  ( is          => 'ro'
  , isa         => 'Username'
  , lazy_build  => 1
  );

has 'pwent' =>
  ( is          => 'ro'
  , isa         => 'ArrayRef'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'password_hash' =>
  ( is          => 'ro'
  , isa         => 'Str'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'home' =>
  ( is          => 'ro'
  , isa         => 'Path'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'config' =>
  ( is          => 'ro'
  , isa         => 'Piny::Config'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'email' =>
  ( is          => 'ro'
  , isa         => 'Piny::Email'
  , lazy_build  => 1
  , init_arg    => undef
  );

has 'groups' =>
  ( is          => 'ro'
  , isa         => 'ArrayRef[Piny::Group]'
  , lazy_build  => 1
  , init_arg    => undef
  );

# Public methods

sub add_group {
  my ( $s, @groups ) = @_;

  foreach my $group ( @groups ) {
    $group->add_member( $s );
  };
};

sub remove_group {
  my ( $s, @groups ) = @_;

  foreach my $group ( @groups ) {
    $group->remove_member( $s );
  };
};

sub has_group {
  my ( $s, $group ) = @_;

  foreach my $owngroup ( @{$s->groups( )} ) {
    return 1 if $owngroup->gid( ) == $group->gid( );
  };

  return;
};

# Class methods

sub all_users {
  my ( $class ) = @_;

  my @ret;

  endpwent( );

  while ( my @info = getpwent( ) ) {
    eval {
      my $user = $class->new( uid => $info[2] );
      # Some forced early evaluation, so error checking happens now.
      $user->name( );
      $user->email( );
      push( @ret, $user );
    };
  };

  endpwent( );

  return @ret;
};

# Builder methods

# If constructed with just one argument, then
#   * If that argument is numeric, treat it as a UID.
#   * Otherwise, treat it as a username.
around BUILDARGS => sub {
  my ( $orig, $class ) = ( shift, shift );

  if ( @_ == 1 && ! ref $_[0] ) {
    if ( $_[0] =~ m/^\d+$/ ) {
      return $class->$orig( uid => $_[0] );
    } else {
      return $class->$orig( name => $_[0] );
    };
  } else {
    return $class->$orig( @_ );
  };
};

sub BUILD {
  my ( $s ) = @_;

  if ( not ( $s->has_uid( ) or $s->has_name( ) ) ) {
    die "You must provide either UID or name!";
  };

  if ( $s->has_uid( ) and $s->has_name( ) ) {
    die "You must not provide both UID and name!";
  };
};

sub _build_uid {
  my ( $s ) = @_;

  return $s->pwent( )->[2];
};

sub _build_name {
  my ( $s ) = @_;

  return $s->pwent( )->[0];
};

sub _build_pwent {
  my ( $s ) = @_;

  if ( $s->has_uid( ) ) {
    my @res = getpwuid( $s->uid( ) );
    die "getpwuid( " . $s->uid( ) . " ) failed: $!" unless @res;
    return \@res;
  } elsif ( $s->has_name( ) ) {
    my @res = getpwnam( $s->name( ) );
    die "getpwnam( " . $s->name( ) . " ) failed: $!" unless @res;
    return \@res;
  } else {
    die "Not enough information provided to lookup user!";
  };
};

sub _build_password_hash {
  my ( $s ) = @_;

  return $s->pwent( )->[1];
};

sub _build_home {
  my ( $s ) = @_;

  return $s->pwent( )->[7];
};

sub _build_config {
  my ( $s ) = @_;

  return Piny::Config->new( confpath => $s->home . "/.gitconfig" );
};

sub _build_email {
  my ( $s ) = @_;

  if ( not defined $s->config->user_email ) {
    die "You must provide a user.email attribute in your .gitconfig!\nPlease run pinyconfig --user user.email your\@email.com";
  };

  return Piny::Email->new( address => $s->config->user_email );
};

sub _build_groups {
  my ( $s ) = @_;

  my @res;
  my @ent;

  endgrent( );

  while ( @ent = getgrent( ) ) {
    next if ( $ent[3] eq "" );
    foreach my $member ( split( / /, $ent[3] ) ) {
      if ( $member eq $s->name( ) ) {
        push @res, Piny::Group->new( gid => $ent[2] );
        last;
      };
    };
  };

  endgrent( );

  return \@res;
};

# Moose boilerplate

__PACKAGE__->meta->make_immutable;

1;
