package LATMOS::Accounts::Bases::Sql::User;

use 5.010000;
use strict;
use warnings;

use LATMOS::Accounts::Utils;
use LATMOS::Accounts::Log;
use POSIX qw(strftime);
use base qw(LATMOS::Accounts::Bases::Sql::objects);

our $VERSION = (q$Rev$ =~ /^Rev: (\d+) /)[0];

=head1 NAME

LATMOS::Ad - Perl extension for blah blah blah

=head1 DESCRIPTION

Account base access over standard unix file format.

=head1 FUNCTIONS

=cut

=head2 new(%config)

Create a new LATMOS::Ad object for windows AD $domain.

domain / server: either the Ad domain or directly the server

ldap_args is an optionnal list of arguments to pass to L<Net::LDAP>.

=cut

sub _object_table { 'user' }

sub _key_field { 'name' }

sub _has_extended_attributes { 1 }

sub _get_attr_schema {
    my ($class, $base) = @_;

    $class->SUPER::_get_attr_schema($base,
        {
            uidNumber => {
                inline => 1,
                iname => 'uidnumber',
                uniq => 1,
                mandatory => 1,
                formopts => { length => 7 },
            },
            uidnumber => { inline => 1, hide => 1, },
            gidNumber => { 
                inline => 1,
                iname => 'gidnumber',
                mandatory => 1,
                can_values => sub {
                    map { $base->get_object('group',
                            $_)->get_attributes('gidNumber') }
                    $base->list_objects('group')
                },
                display => sub {
                    my ($self, $val) = @_;
                    my ($gr) = $self->base->search_objects('group', "gidNumber=$val")
                        or return;
                    return $gr;
                },
                reference => 'group',
            },
            loginShell => { mandatory => 1 },
            gidnumber => { inline => 1, hide => 1,
                can_values => sub {
                    map { $_->get_attributes('gidNumber') }
                    map { $base->get_object('group', $_) }
                    $base->list_objects('group')
                },
                mandatory => 1,
                reference => 'group',
            },
            exported  => {
                inline => 1,
                formtype => 'CHECKBOX',
            },
            locked    => {
                formtype => 'CHECKBOX',
                formopts => { rawvalue => 1, },
            },
            expire    => { inline => 1, formtype => 'DATE', },
            name      => { inline => 1, ro => 1, },
            cn        => {
                inline => 1, ro => 1,
                get => sub {
                    my ($self) = @_;
                    return join(' ', grep { $_ } 
                        (
                            $self->object->_get_c_field('givenName'),
                            $self->object->_get_c_field('sn')
                        )
                    )
                    || $self->object->_get_c_field('description')
                    || $self->object->id;
                },
            },
            create    => { inline => 1, ro => 1, },
            date      => { inline => 1, ro => 1, },
            memberOf  => {
                multiple => 1, delayed => 1,
                get => sub {
                    my ($self) = @_;
                    my $obj = $self->object;
                    my $sth = $obj->db->prepare_cached(
                        q{
                        select name from "group" join
                        group_attributes on group_attributes.okey = "group".ikey
                        where value = ? and attr = ?
                        }
                    );
                    $sth->execute($obj->id, 'memberUID');
                    my @res;
                    while (my $res = $sth->fetchrow_hashref) {
                        push(@res, $res->{name});
                    }
                    return \@res;
                }
            },
            forward   => {},
            aliases   => {
                reference => 'aliases',
                formtype => 'TEXT',
                multiple => 1,
            },
            revaliases => {
                formtype => 'TEXT',
            },
            manager => {
                delayed => 1,
                can_values => sub {
                    my %uniq = map { $_ => 1 } grep { $_ }
                    ($_[1] ? $_[1]->get_attributes('manager') : ()),
                    $base->search_objects('user', 'active=*');
                    sort keys %uniq;
                },
                reference => 'user',
            },
            department => {
                reference => 'group',
                can_values => sub {
                    $base->search_objects('group', 'sutype=dpmt')
                }
            },
            contratType => {
                reference => 'group',
                can_values => sub {
                    $base->search_objects('group', 'sutype=contrattype')
                }
            },
            site => {
                reference => 'site',
                can_values => sub {
                    $base->search_objects('site')
                }
            },
            co => { },
            l => { },
            postalCode => { },
            streetAddress => { formtype => 'TEXTAREA', },
            postOfficeBox => { },
            st => { },
            facsimileTelephoneNumber => { },
            o => { },
            telephoneNumber => { },
            physicalDeliveryOfficeName => { },
            uid => { iname => 'name', ro => 1 },
            cn =>  { iname => 'name', ro => 1 },
            gecos => {
                ro => 1,
                get => sub {
                    my ($self) = @_;
                    my $obj = $self->object;
                    my $gecos = sprintf("%s,%s,%s,%s",
                        join(' ', grep { $_ }
                                ($obj->_get_c_field('givenName'),
                                ($obj->_get_c_field('sn'))))
                            || $obj->_get_c_field('description') || '',
                        join(' - ', grep { $_ } (($obj->_get_c_field('site') ||
                                    $obj->_get_c_field('l')),
                            $obj->_get_c_field('physicalDeliveryOfficeName'))) || '',
                        $obj->_get_c_field('telephoneNumber') || '',
                        $obj->_get_c_field('expireText') || '',
                    );
                    $gecos =~ s/:/ /g;
                    return to_ascii($gecos);
                },
            },
            displayName  => {
                ro => 1, managed => 1,
                get => sub {
                    my ($self) = @_;
                    return join(' ', grep { $_ } 
                        (
                            $self->object->_get_c_field('givenName'),
                            $self->object->_get_c_field('sn')
                        )
                    )
                    || $self->object->_get_c_field('description')
                    || $self->object->id;
                },
            },
            sAMAccountName  => { ro => 1, managed => 1  },
            accountExpires => {
                ro => 1,
                managed => 1,
                get => sub {
                    my ($self) = @_;
                    my $obj = $self->object;
                    my $sth = $obj->db->prepare_cached(
                        sprintf(
                            q{select extract(epoch from expire) + 11644474161 as expire
                            from %s where %s = ?},
                            $obj->db->quote_identifier($obj->_object_table),
                            $obj->db->quote_identifier($obj->_key_field),
                        )
                    );
                    $sth->execute($obj->id);
                    my $res = $sth->fetchrow_hashref;
                    $sth->finish;
                    return $res->{expire} ? sprintf("%.f", $res->{expire} * 1E7) : '9223372036854775807';
                }
            },
            shadowExpire => {
                ro => 1,
                managed => 1,
                get => sub {
                    my ($self) = @_;
                    my $obj = $self->object;
                    my $sth = $obj->db->prepare_cached(
                        sprintf(
                            q{select justify_hours(expire - '1/1/1970'::timestamp) as expire
                            from %s where %s = ?},
                            $obj->db->quote_identifier($obj->_object_table),
                            $obj->db->quote_identifier($obj->_key_field),
                        )
                    );
                    $sth->execute($obj->id);
                    my $res = $sth->fetchrow_hashref;
                    $sth->finish;
                    return -1 unless($res->{expire});
                    $res->{expire} =~ /(\d+) days\s*(\w)?/;
                    return $1 + ($2 ? 1 : 0);
                }
            },
            directReports => {
                reference => 'user',
                ro => 1,
                delayed => 1,
                get => sub {
                    my ($self) = @_;
                    my $obj = $self->object;
                    my $sth = $obj->db->prepare_cached(
                        q{
                        select name from "user" join
                        user_attributes on user_attributes.okey = "user".ikey
                        where value = ? and attr = ?
                        }
                    );
                    $sth->execute($obj->id, 'manager');
                    my @res;
                    while (my $res = $sth->fetchrow_hashref) {
                        push(@res, $res->{name});
                    }
                    return \@res;
                },
            },
            managedObjects => { ro => 1, reference => 'group', },
            otheraddress => { ro => 1, reference => 'address', },
            mainaddress => { ro => 1, reference => 'address', },
            postalAddress => { ro => 1, },
            facsimileTelephoneNumber => { ro => 1, },
            allsite   => {
                ro => 1,
                reference => 'site',
            },
            managerContact => {
                ro => 1,
                reference => 'user',
                get => sub {
                    my ($self) = @_;
                    if (my $manager = $self->object->_get_c_field('manager')) {
                        return $manager;
                    } elsif (my $department = $self->object->_get_c_field('department')) {
                        my $obj = $self->base->get_object('group', $department);
                        return $obj->_get_c_field('managedBy');
                    } else {
                        return;
                    }
                },
            },
            expireText => {
                ro => 1,
                get => sub {
                    my ($self) = @_;
                    my $obj = $self->object;
                    my $sth = $obj->db->prepare_cached(
                        sprintf(
                            q{select to_char(expire, 'YYYY/MM/DD') as expire
                            from %s where %s = ?},
                            $obj->db->quote_identifier($obj->_object_table),
                            $obj->db->quote_identifier($obj->_key_field),
                        )
                    );
                    $sth->execute($obj->id);
                    my $res = $sth->fetchrow_hashref;
                    $sth->finish;
                    return $res->{expire}
                },
            },
            krb5ValidEnd => { ro => 1, },
            cells  => {
                ro => 1,
                reference => 'group',
            },
            departments => {
                reference => 'group',
                delayed => 1,
                ro => 1,
            },
            arrivalDate => { },
            expired => { ro => 1 },
            active => { ro => 1 },
            pwdAccountLockedTime => {
                managed => 1,
                ro => 1,
                get => sub {
                    my ($self) = @_;
                    my $obj = $self->object;
                    if ($obj->_get_c_field('locked')) {
                        return '000001010000Z';
                    } else {
                        my $sth = $obj->db->prepare_cached(
                            sprintf(
                                q{select to_char(expire AT TIME ZONE 'Z', 'YYYYMMDDHH24MISSZ') as expire
                                from %s where %s = ? and expire < now()},
                                $obj->db->quote_identifier($obj->_object_table),
                                $obj->db->quote_identifier($obj->_key_field),
                            )
                        );
                        $sth->execute($obj->id);
                        my $res = $sth->fetchrow_hashref;
                        $sth->finish;
                        return $res->{expire}
                    }
                },
            },
            userPassword => { readable => 0, },
        }
    )
}

sub get_field {
    my ($self, $field) = @_;
    if ($field eq 'sAMAccountName') {
        return $self->id;
    } elsif ($field eq 'krb5ValidEnd') {
        my $sth = $self->db->prepare_cached(
            sprintf(
                q{select date_part('epoch', expire)::int as expire
                from %s where %s = ?},
                $self->db->quote_identifier($self->_object_table),
                $self->db->quote_identifier($self->_key_field),
            )
        );
        $sth->execute($self->id);
        my $res = $sth->fetchrow_hashref;
        $sth->finish;
        return $res->{expire}
    } elsif ($field eq 'pwdAccountLockedTime') {
    } elsif ($field eq 'otheraddress') {
        my $sth = $self->db->prepare_cached(q{
            select name from address left join address_attributes
            on address.ikey = address_attributes.okey and
            address_attributes.attr = 'isMainAddress'
            where "user" = ?
            order by address_attributes.attr
        });
        $sth->execute($self->id);
        my @values;
        while (my $res = $sth->fetchrow_hashref) {
            push(@values, $res->{name});
        }
        return \@values;
    } elsif ($field eq 'mainaddress') {
        my $sth = $self->db->prepare_cached(q{
            select name from address join address_attributes on ikey = okey
            where "user" = ? and attr = 'isMainAddress'
            });
        $sth->execute($self->id);
        my $res = $sth->fetchrow_hashref;
        $sth->finish;
        return $res->{name};
    } elsif (grep { $field eq $_ } qw(postalAddress
            co l postalCode streetAddress 
            postOfficeBox st 
            facsimileTelephoneNumber 
            o telephoneNumber 
            physicalDeliveryOfficeName 
            site
        )) {
        if (my $fmainaddress = $self->_get_c_field('mainaddress')) {
            my $address = $self->base->get_object('address', $fmainaddress);
            if ($address) {
                return $address->_get_c_field($field);
            } else { # can't happend
                return;
            }
        } else {
            return $self->SUPER::get_field($field);
        }
    } elsif ($field eq 'aliases') {
        my $sth = $self->db->prepare(q{
            select name from aliases where array[lower($1)] =
                string_to_array(lower(array_to_string("forward", ',')), ',')
        } . ($self->base->{wexported} ? '' : 'and exported = true'));
        $sth->execute($self->id);
        my @values;
        while (my $res = $sth->fetchrow_hashref) {
            push(@values, $res->{name});
        }
        return \@values;
    } elsif ($field eq 'forward') {
        my $sth = $self->db->prepare(q{
            select forward from aliases where name = ?
        } . ($self->base->{wexported} ? '' : ' and exported = true'));
        $sth->execute($self->id);
        my $res = $sth->fetchrow_hashref;
        $sth->finish;
        return $res->{forward}
    } elsif ($field eq 'revaliases') {
        my $sth = $self->db->prepare(q{
            select "as" from revaliases where name = ?
        } . ($self->base->{wexported} ? '' : ' and exported = true'));
        $sth->execute($self->id);
        my $res = $sth->fetchrow_hashref;
        $sth->finish;
        return $res->{as}
    } elsif ($field eq 'managerContact') {
    } else {
        return $self->SUPER::get_field($field);
    }
}

sub _get_state {
    my ($self, $state) = @_;
    for ($state) {
        /^expired$/ and do {
            my $attribute = $self->attribute('expire');
            $attribute->check_acl('r') or return;
            my $sth = $self->db->prepare_cached(
                q{ select coalesce(expire < now(), false) as exp from "user" 
                where "user".name = ?}
            );
            $sth->execute($self->id);
            my $res = $sth->fetchrow_hashref;
            $sth->finish;
            return $res->{exp} ? 1 : 0;
        };
    }
}

sub set_fields {
    my ($self, %data) = @_;
    my %fdata;
    my $res = 0;
    foreach my $attr (keys %data) {
        $attr =~ /^(un)?exported$/ and do {
            if (my $obj = $self->base->
                get_object('revaliases', $self->id)) {
                my $ares = $obj->set_c_fields(
                    ($attr eq 'exported' ? 'exported' : 'unexported') => $data{$attr}
                );
                if (defined($ares)) {
                    $res+=$ares;
                } else {
                    $self->base->log(LA_ERR,
                        'Cannot set revaliases exported attribute for user %s',
                        $self->id);
                }
            }
            my $must_expire = $attr eq 'exported'
                ? ($data{$attr} ? 0 : 1 )
                : ($data{$attr} ? 1 : 0 );

            foreach my $al ($self->get_attributes('aliases')) {
                my $obj = $self->base->get_object('aliases', $al) or next;
                $obj->_set_c_fields(
                    expire => $must_expire
                        ? strftime(
                            "%Y-%m-%d %H:%M:%S",
                            localtime(time + 3600 * 24 * 365)
                          )
                        : undef,
                );
            }
        };
        $attr eq 'gidnumber' && $data{$attr} !~ /^\d+$/ and do {
            my $group = $self->base->get_object('group', $data{$attr}) or do {
                $self->base->log(LA_ERROR,
                    "Can't set gidNumber to %s: no such group", $data{$attr});
                return;
            };
            $data{$attr} = $group->get_attributes('gidNumber');
        };
        $attr =~ /^memberOf$/ and do {
            my %member;
            my $memberof = $self->get_field('memberOf');
            foreach (ref $memberof
                ? @{ $memberof }
                : $memberof || ()) {
                $member{$_}{c} = 1;
            }
            foreach (grep { $_ } ref $data{$attr} ? @{ $data{$attr} || []} : $data{$attr}) {
                $member{$_}{n} = 1;
            }

            foreach (keys %member) {
                $member{$_}{c} && $member{$_}{n} and next; # no change !
                my $group = $self->base->get_object('group', $_) or next;
                ($group->_get_c_field('sutype') || '') =~ /^(jobtype|contrattype)$/ and next;
                if ($member{$_}{n}) {
                    my $sth = $self->db->prepare_cached(
                        q{insert into group_attributes_users (value, attr, okey) values (?,?,?)}
                    );
                    $sth->execute($self->id, 'memberUID', $group->_get_ikey);
                    $res++;
                } elsif ($member{$_}{c}) {
                    if (($self->get_c_field('department') || '') eq $group->id) {
                        $self->base->log(LA_WARN,
                            "Don't removing user %s from group %s: is it's department",
                            $self->id, $group->id);
                        next;
                    }
                    my $sth = $self->db->prepare_cached(
                        q{delete from group_attributes_users where value = ? and attr = ? and okey = ?}
                    );
                    $sth->execute($self->id, 'memberUID', $group->_get_ikey);
                    $res++;
                } # else {} # can't happend
            }
            next;
        };
        $attr =~ /^forward$/ and do {
            if ($data{$attr}) {
                if (my $f = $self->base->get_object('aliases', $self->id)) {
                    $res += $f->_set_c_fields(forward => $data{$attr});
                } else {
                    if ($self->base->_create_c_object(
                            'aliases', $self->id,
                            forward => $data{$attr},
                            description => 'automatically created for ' . $self->id,
                        )) {
                        $res++;
                    } else {
                        $self->base->log(LA_ERR, "Cannot add forward for %s",
                            $self->id);
                    }
                }
            } else {
                if ($self->base->_delete_object('aliases', $self->id)) {
                    $res++;
                } else {
                    $self->base->log(LA_ERR, "Cannot remove forward for %s",
                        $self->id);
                }
            }
            next;
        };
        $attr =~ /^aliases$/ and do {
            my %aliases = map { $_ => 1 } grep { $_ } (ref $data{$attr} ? @{$data{$attr}} :
                $data{$attr});
            foreach ($self->_get_attributes('aliases')) {
                $aliases{$_} ||= 0;
                $aliases{$_} +=2;
            }
            foreach (keys %aliases) {
                if ($aliases{$_} == 2) {
                    if ($self->base->_delete_object('aliases', $_)) {
                        $res++
                    } else {
                        $self->base->log(LA_ERR,
                            "Cannot remove aliases %s from user %s", $_,
                            $self->id);
                    }
                } elsif ($aliases{$_} == 1) {
                    if ($self->base->_create_c_object(
                            'aliases', $_,
                            forward => [ $self->id ],
                            description => 'automatically created for ' . $self->id,
                        )) {
                        $res++
                    } else {
                        $self->base->log(LA_ERR, 'Cannot set forward %s to user %s',
                            $_, $self->id);
                        return
                    }
                } # 3 no change
            }
            next;
        };
        $attr =~ /^revaliases$/ and do {
            if ($data{$attr}) {
                if (my $obj = $self->base->
                        get_object('revaliases', $self->id)) {
                    my $ares = $obj->set_c_fields(
                        'as' => $data{$attr},
                        'exported' => ($self->get_attributes('exported') || 0),
                    );
                    if (defined($ares)) {
                        $res+=$ares;
                    } else {
                        $self->base->log(LA_ERR, 'Cannot set revaliases for user %s',
                            $self->id);
                    }
                } else {
                    if ($self->base->_create_c_object(
                        'revaliases',
                        $self->id, as => $data{$attr},
                        'exported' => ($self->get_attributes('exported') || 0),
                        description => 'automatically created for ' . $self->id,
                    )) {
                        $res++;
                    } else {
                        $self->base->log(LA_ERR, 'Cannot set revaliases for user %s',
                            $self->id);
                    }
                }
            } else {
                $self->base->_delete_object('revaliases', $self->id);
                $res++;
            }
            next;
        };
        $attr =~ /^department$/ and do {
            if ($data{$attr}) {
                my $dpmt = $self->base->get_object('group', $data{$attr}) or do {
                    $self->base->log(LA_ERR, 
                        "Group %s does not exists",
                        $data{$attr});
                    return;
                };
                if ((($dpmt->_get_c_field('sutype') || '') ne 'dpmt')) {
                    $self->base->log(LA_ERR, "Group %s is not a department",
                        $data{$attr});
                    return;
                }
            }
        };          
        $attr =~ /^jobType$/ and do {
            if ($data{$attr}) {
                my $dpmt = $self->base->get_object('group', $data{$attr}) or do {
                    $self->base->log(LA_ERR, 
                        "Group %s does not exists",
                        $data{$attr});
                    return;
                };
                if ((($dpmt->_get_c_field('sutype') || '') ne 'jobtype')) {
                    $self->base->log(LA_ERR, "Group %s is not a jobtype",
                        $data{$attr});
                    return;
                }
            }
        };          
        $attr =~ /^contratType$/ and do {
            if ($data{$attr}) {
                my $dpmt = $self->base->get_object('group', $data{$attr}) or do {
                    $self->base->log(LA_ERR, 
                        "Group %s does not exists",
                        $data{$attr});
                    return;
                };
                if ((($dpmt->_get_c_field('sutype') || '') ne 'contrattype')) {
                    $self->base->log(LA_ERR, "Group %s is not a contrattype",
                        $data{$attr});
                    return;
                }
            }
        };          
        grep { $attr eq $_ } (qw(co l postalCode streetAddress 
            postOfficeBox st facsimileTelephoneNumber 
            o telephoneNumber physicalDeliveryOfficeName site)) and do {
            my $fmainaddress = $self->_get_c_field('mainaddress');
            # set address attribute => create address object on the fly
            # except if attr is empty !
            if (!$fmainaddress && $data{$attr}) {
                $fmainaddress = $self->id . '-' . join('', map { ('a'..'z')[rand(26)] }
                (0..4));
                $self->base->_create_c_object(
                    'address', $fmainaddress,
                    user => $self->id,
                    isMainAddress => 1, ) or do {
                    $self->base->log(LA_ERR,
                        "Cannot create main address for user %s", $self->id);
                    return;
                };
            }
            if ($fmainaddress && 
                (my $address = $self->base->get_object('address', $fmainaddress))) {
                if ($address->attribute($attr) &&
                    !$address->attribute($attr)->ro) {
                    $res += $address->set_c_fields($attr => $data{$attr}) ||0;
                }
            }
            next;
        };
        $fdata{$attr} = $data{$attr} || undef;
    }
    if (keys %fdata) {
        if (defined(my $res2 = $self->SUPER::set_fields(%fdata))) {
           return $res2 + $res;
       } else {
           return;
       }
    } else { return $res; }
}


1;

__END__

=head1 SEE ALSO

=head1 AUTHOR

Olivier Thauvin, E<lt>olivier.thauvin@latmos.ipsl.frE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2008, 2009 CNRS SA/CETP/LATMOS

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.10.0 or,
at your option, any later version of Perl 5 you may have available.

=cut
