package Newslash::Model::Comments;
use Newslash::Model::Base -base;

use Data::Dumper;
use Mojo::Log;

use bytes qw();
use Digest::MD5 qw(md5_hex);
use Encode;

sub count {
    my $self = shift;
    my $join = 'LEFT JOIN firehose ON (comments.cid = firehose.srcid AND firehose.type = "comment")';
    my $where = 'firehose.public != "no"';
    return $self->generic_count(table => "comments",
                                target => "cid",
                                timestamp => "date",
                                join => $join,
                                where => $where,
                                @_);
}

#========================================================================

=head2 select($query_type, $value)

get comments.

=over 4

=item Parameters

=over 4

=item $query_type

query key, one of below.

  "stoid": story ID (int)
  "cid": comment ID
  "pid": parent comment ID

  Warning: stoid is unique int ID for story, not strings value (/yy/mm/xxxxxx style).
           ... In comments table, I don't know why, use 'sid' column name for int ID value.
           But in this function, use "stoid" for consistency.

=item $value

value for query

=back

=item Return value

ARRAY of comment contents

=back

=cut

sub select {
    my $self = shift;
    my $params = {@_};

    my $unique_keys = { id => "comments.cid",
                        comment_id => "comments.cid",
                        cid => "comments.cid",
                      };
    my $keys = { pid => "comments.pid",
                 parent_id => "comments.pid",
                 discussion_id => "comments.sid",
                 uid => "comments.uid",
                 user_id => "comments.uid",
                 points => "comments.points",
                 create_time => "comments.date",
                 update_time => "comments.last_update",
               };
    my $datetime_keys = { create_time => "comments.date",
                          update_time => "comments.last_update",
                        };
    my $timestamp = "comments.date";

    my ($where_clause, $where_values, $unique) = $self->build_where_clause(unique_keys => $unique_keys,
                                                                           keys => $keys,
                                                                           datetime_keys => $datetime_keys,
                                                                           timestamp => $timestamp,
                                                                           params => $params);
    my ($limit_clause, $limit_values) = $self->build_limit_clause(params => $params);
    my ($orderby_clause, $orderby_values) = $self->build_order_by_clause(keys => $keys,
                                                                         params => $params);

    $orderby_clause = "ORDER BY comments.cid ASC" if !$orderby_clause;

    # TODO: give reasonable LIMIT Value...
    $limit_clause = "LIMIT 1000" if !$limit_clause;

    my @attrs;
    push @attrs, @$where_values, @$limit_values, @$orderby_values;
    my $dbh = $self->connect_db;

    # `ORDER BY comments.cid ASC` is needed by comment-tree-building.
    # if change there, must fix other codes.
    my $sql = <<"EOSQL";
SELECT comments.*, comment_text.*, users.nickname AS author, users.sig AS signature
  FROM comments
    LEFT JOIN comment_text ON comments.cid = comment_text.cid
    LEFT JOIN users ON comments.uid = users.uid
  $where_clause
  $orderby_clause
  $limit_clause
EOSQL

    my $sth = $dbh->prepare($sql);

    $sth->execute(@attrs);
    my $comments = $sth->fetchall_arrayref(+{});
    if (!defined $comments) {
        $self->set_error($dbh->errstr, $dbh->err);
        $self->disconnect_db;
        return;
    }
    $self->disconnect_db;

    if (!$comments) {
        return;
    }

    my $sec_lv = $params->{security_level} || 0;

    if ($unique) {
        my $c = $comments->[0];
        $self->_generalize($c);
        return $c;
    }

    for my $c (@$comments) {
        $self->_generalize($c);
    }
    return $comments;
}

sub build_comment_tree {
    my ($self, $comments)  = @_;
    my $root = [];
    my $comment_of = {};

    # prerequisite: $comments are sorted with ascendent order by cid
    for my $comment (@$comments) {
        my $pid = $comment->{pid};
        my $cid = $comment->{cid};
        $comment_of->{$cid} = $comment;
        $comment->{children} = [];

        if ($pid == 0) {
            push @$root, $comment;
            next;
        }

        my $parent = $comment_of->{$pid};
        if ($parent) {
            push @{$parent->{children}}, $comment;
        } else {
            warn("(Controller::Story) invalid comment order. cid: $cid, pid: $pid");
        }
    }
    return $root;
}

sub generate_signature {
    my ($self, $comment) = @_;
    return md5_hex(encode_utf8($comment));
}

sub _generalize {
    my ($self, $c) = @_;

    # NTO-nized
    $c->{id} = $c->{cid};
    $c->{comment_id} = $c->{cid};
    $c->{create_time} = $c->{date};
    $c->{update_time} = $c->{last_update};
    $c->{content_type} = "comment";
    $c->{title} = $c->{subject};
    $c->{introtext} = $c->{comment};
    $c->{intro_text} = $c->{comment};
    $c->{full_text} = $c->{intro_text};
    $c->{fulltext} = $c->{full_text};
    $c->{public} => "yes";

    $c->{discussion_id} = $c->{sid};

    $c->{parent_id} = $c->{pid};
}


#========================================================================

=head2 create(\%params, \%options)

create comment

see saveComment in Slass/Utility/Comments/Comments.pm too.

=over 4

=item Parameters

=over 4

=item \%options

comment parameters

 * sid
 * pid
 * title
 * comment
 * tags

=item \%options

options for submission

=back

=item Return value

ARRAY of story contents

=back

=cut

sub create {
    my ($self, $params, $user, $extra_params, $opts) = @_;
    if (!$params || !$user) {
        $self->set_error("no params or no user given!");
        return;
    }
    return if $self->check_readonly;

    $opts ||= {};
    $extra_params ||= {};

    # check vital parameters are exists
    if (!$params->{discussion_id}
        || !$params->{title}
        || !$params->{comment}
        || !$user) {
        $self->set_error("vital parameters not given!");
        return;
    }

    # if pid given, validate them
    my $subject_orig = 'yes';
    $params->{pid} ||= 0;
    if ($params->{pid}) {
        my $parent = $self->select(cid => $params->{pid});

        if (!$parent) {
            $self->set_error("parent comment not exists", 100);
            return;
        }
        if ($parent->{sid} != $params->{discussion_id}) {
            $self->set_error("parent comment belongs another story", 101);
            return;
        }
        # TODO: check visibility
        # check title is reply?
        if ($params->{title} =~ /$parent->{subject}/) {
            $subject_orig = 'no';
        }
    }
    my $subject_max_length = 50;
    my $subject = substr $params->{title}, 0, $subject_max_length;
    my $signature = $self->generate_signature($params->{comment});

    my $users = $self->new_instance_of("Newslash::Model::Users");
    #use Newslash::Model::Moderations;
    my $moderations = $self->new_instance_of("Newslash::Model::Moderations");
    my $user_param = $users->param->select(uid => $user->{uid});

    my $nobonus = exists $params->{nobonus} ? $params->{nobonus} : ($user_param->{nobonus} || 0);
    my $scores = $moderations->score_for_comment_post($user);

    my $sql = <<"EOSQL";
INSERT INTO comments
    (sid, pid, date,  ipid, subnetid, subject, subject_orig, uid, points, pointsorig,
     pointsmax, signature, len, karma_bonus, karma, karma_abs, tweak_orig, tweak)
  VALUES
    (?,   ?,   NOW(), ?,    ?,        ?,       ?,            ?,   ?,      ?,
     ?,         ?,         ?,   ?,           ?,     ?,         ?,          ?);
EOSQL

    $params->{ipid} = $user->{ipid};
    $params->{subnetid} = $user->{subnetid};
    $params->{subject} = $subject;
    $params->{subject_orig} = $subject_orig;
    $params->{uid} = $user->{uid};
    $params->{points} = $scores->{points};
    $params->{pointsorig} = $scores->{points};
    $params->{pointsmax} = $scores->{points};
    $params->{len} = bytes::length $params->{comment};
    $params->{karma} ||= 0;
    $params->{karma_abs} ||= 0;
    $params->{karma_bonus} = $scores->{karma_bonus};
    $params->{tweak_orig} = $scores->{tweak};
    $params->{tweak} = $scores->{tweak};

    #my $dbh = $self->connect_db({AutoCommit => 0,});
    my $dbh = $self->start_transaction;
    my $rs = $dbh->do($sql, undef,
                      $params->{discussion_id},
                      $params->{pid},
                      $params->{ipid},
                      $params->{subnetid},
                      $params->{subject},
                      $params->{subject_orig},
                      $params->{uid},
                      $params->{points}, #points
                      $params->{pointsorig}, #pointsorig
                      $params->{pointsmax}, #pointsmax
                      $signature, #signature,
                      $params->{len},
                      $params->{karma_bonus}, #karma_bonus
                      $params->{karma},
                      $params->{karma_abs},
                      $params->{tweak_orig}, #tweak_orig
                      $params->{tweak} #tweak
                     );
    if (!$rs) {
        # error occured
        $self->set_error("mysql_error", $dbh->{mysql_errno});
        #$dbh->rollback;
        #$dbh->disconnect;
        $self->rollback;
        return undef;
    }
    my $cid = $dbh->last_insert_id(undef, undef, undef, undef);
    $sql = "INSERT INTO comment_text (cid, comment) VALUE (?, ?)";
    $rs = $dbh->do($sql, undef, $cid, $params->{comment});
    if (!$rs) {
        # error occured
        $self->set_error("mysql_error", $dbh->{mysql_errno});
        #$dbh->rollback;
        #$dbh->disconnect;
        $self->rollback;
        return undef;
    }

    # update discussion
    $sql = "UPDATE discussions SET commentcount = commentcount + 1 WHERE id = ?";
    $rs = $dbh->do($sql, undef, $params->{discussion_id});
    if (!$rs) {
        # error occured
        $self->set_error("mysql_error", $dbh->{mysql_errno});
        #$dbh->rollback;
        #$dbh->disconnect;
        $self->rollback;
        return undef;
    }

    # check and undo moderation
    if ($user->{is_login}
        && !$user->{is_admin}
        && !$user->{author}) {
        my $discussions = $self->new_instance_of("Moderations");
        my $undo = $moderations->undo_moderation(discussion_id => $params->{discussion_id}, $user->{uid});
        if ($undo) {
            $self->{undo_moderation} = 1;
        }
    }

    use Newslash::Model::Vars;
    my $vars = $self->new_instance_of("Newslash::Model::Vars");
    $vars->add('totalComments', 1);

    #my $stories = $self->new_instance_of("Newslash::Model::Stories");
    # update below story parameter.
    # $stories->set_dirty(stoid => $params->{stoid});
    my $slash_db = $self->new_instance_of("Newslash::Model::SlashDB");
    $slash_db->setStory($params->{discussion_id}, { writestatus => 'dirty' });

    # TODO:
    # in slash's original implement, stories.commentcount and stories.hitparade is not
    # updated here. they are updated by slashd.

    if ($user->{is_login}) {
        my $users = $self->new_instance_of("Newslash::Model::Users");
        $users->info->update(uid => $user->{uid},
                             totalcomments => {add => 1});
    }

    #$dbh->commit;
    #$dbh->disconnect;
    $self->commit;

    # create firehose item
    $params->{cid} = $cid;
    my $firehose = $self->new_instance_of("Newslash::Model::LegacyFirehose");
    $firehose->createItemFromComment($cid, $params);
    #$self->firehose_createItemFromComment($cid, $params);

    # saveCommentInfo is deprecated.
    # $slashdb->saveCommentInfo($maxCid, $submit_type);

    # TODO: check cid is overflowed?

    # TODO: messaging
    # TODO: achievements

    return $cid;
}

sub firehose_createItemFromComment {
    my($self, $cid, $comment) = @_;
    return if $self->check_readonly;

    #my $comment = $self->getComment($cid);
    #my $text = $self->getCommentText($cid);

    my $globjs = $self->new_instance_of("Newslash::Model::Globjs");
    #my $globjid = $self->getGlobjidCreate("comments", $cid);
    my $globjid = $globjs->getGlobjidCreate("comments", $cid);


    my $firehose = $self->new_instance_of("Newslash::Model::LegacyFirehose");
    # Set initial popularity scores -- we'll be forcing a quick
    # recalculation of them so these scores don't much matter.
    my ($popularity, $editorpop, $neediness);
    #$popularity = $self->getEntryPopularityForColorLevel(7);
    #$editorpop = $self->getEntryPopularityForColorLevel(7);
    #$neediness = $self->getEntryPopularityForColorLevel(6);
    $popularity = $firehose->getEntryPopularityForColorLevel(7);
    $editorpop = $firehose->getEntryPopularityForColorLevel(7);
    $neediness = $firehose->getEntryPopularityForColorLevel(6);

    my $data = {
                uid             => $comment->{uid},
                public          => "yes",
                title           => $comment->{subject},
                introtext       => $comment->{comment},
                ipid            => $comment->{ipid},
                subnetid        => $comment->{subnetid},
                type            => "comment",
                srcid           => $comment->{cid},
                popularity      => $popularity,
                editorpop       => $editorpop,
                globjid         => $globjid,
                discussion      => $comment->{sid},
                createtime      => $comment->{date},
               };
    #my $fhid = $self->createFireHose($data);
    my $fhid = $firehose->createFireHose($data);

    # TODO: tags?
    # if (!isAnon($comment->{uid})) {
    #     my $constants = getCurrentStatic();
    #     my $tags = getObject('Slash::Tags');
    #     $tags->createTag({
    #                       uid                     => $comment->{uid},
    #                       name                    => $constants->{tags_upvote_tagname},
    #                       globjid                 => $globjid,
    #                       private                 => 1,
    #                      });
    # }
    #
    # my $tagboxdb = getObject('Slash::Tagbox');
    # if ($tagboxdb) {
    #     for my $tbname (qw( FireHoseScores FHEditorPop CommentScoreReason )) {
    #         my $tagbox = $tagboxdb->getTagboxes($tbname);
    #         next unless $tagbox;
    #         $tagbox->{object}->forceFeederRecalc($globjid);
    #     }
    # }

    return $fhid;
}


#========================================================================

=head2 update

update comment infomation.

=over 4

=item Return value

HASH

=back

=cut

sub update {
    my $self = shift;
    return if $self->check_readonly;

    my $params = {@_};
    my $cid = $params->{cid};
    return if !$cid;

    my $user = $params->{user};

    #my $columns = qw(points lastmod reason);

    my @values;
    my @set_clauses;

    if (defined $params->{points}) {
        if (!ref($params->{points})) {
            push @values, $params->{points};
            push @set_clauses, "points = ?";

            # update pointsmax column
            push @values, $params->{points};
            push @set_clauses, "pointsmax = GREATEST(pointsmax, ?)";
        }
        elsif (ref($params->{points}) eq 'HASH') {
            my $p = $params->{points};
            if ($p->{add}) {
                if (defined $p->{max} && defined $p->{min}) {
                    push @values, $p->{min};
                    push @values, $p->{max};
                    push @values, $p->{add};
                    push @set_clauses, "points = GREATEST(?, LEAST(?, points + ?))";

                # update pointsmax column
                    push @values, $p->{min};
                    push @values, $p->{max};
                    push @values, $p->{add};
                    push @set_clauses, "pointsmax = GREATEST(pointsmax, GREATEST(?, LEAST(?, points + ?)))";
                }
                else {
                    push @values, $p->{add};
                    push @set_clauses, "points = points + ?";

                # update pointsmax column
                    push @values, $p->{add};
                    push @set_clauses, "pointsmax = GREATEST(pointsmax, points + ?)";
                }
            }
        }
    }

    for my $column (qw(karma karma_abs tweak)) {
        if ($params->{$column}) {
            if (ref($params->{$column}) eq 'SCALAR') {
                push @values, $params->{$column};
                push @set_clauses, "$column = ?";
            }
            elsif (ref($params->{$column}) eq 'HASH') {
                if ($params->{$column}->{add}) {
                    push @values, $params->{$column}->{add};
                    push @set_clauses, "$column = $column + ?";
                }
            }
        }
    }

    for my $column (qw(reason lastmod karma_bonus)) {
        if ($params->{$column}) {
            push @values, $params->{$column};
            push @set_clauses, "$column = ?";
        }
    }

    return 0 if !@set_clauses;
    my $set_clause = join(", ", @set_clauses);

    my $where_clause = "cid = ?";
    push @values, $cid;

    if ($params->{lastmod}) {
        $where_clause = $where_clause . "AND lastmod != ?";
        push @values, $params->{lastmod};
    }

    my $sql = "UPDATE comments SET $set_clause WHERE $where_clause";
    my $dbh = $self->connect_db;
    my $rs = $dbh->do($sql, undef, @values);
    if (!$rs) {
        $self->disconnect_db;
        return $rs;
    }
    $self->disconnect_db;
    return 1;
}

# delete comment from database
# this method is for test purpose only.
sub hard_delete {
    my $self = shift;
    return if $self->check_readonly;
    my $params = {@_};

    my $cid = $params->{cid};
    my $discuss_id = $params->{discussion_id};
    return if !$cid && !$discuss_id;

    my $error = 0;
    my $sql;

    my $dbh = $self->start_transaction;

    if ($discuss_id) {
        # first, delete comment_text
        my $sql = "DELETE FROM comment_text WHERE cid = (SELECT cid FROM comments WHERE sid = ?)";
        my $rs = $dbh->do($sql, undef, $discuss_id);
        if (!defined $rs || $rs == 0) {
            Mojo::Log->new->warn("DELETE FROM comment_text failed. cid is $cid.");
            $error = 1;
        }

        # delete comments
        if (!$error) {
            $sql = "DELETE FROM comments WHERE sid = ?";
            $rs = $dbh->do($sql, undef, $discuss_id);
            if (!defined $rs || $rs == 0) {
                Mojo::Log->new->warn("DELETE FROM comments failed. cid is $cid.");
                $error = 1;
            }
        }
        # todo: delete from firehose
    }

    if ($cid) {
        for my $table (qw(comments comment_text)) {
            my $sql = "DELETE FROM $table WHERE cid = ?";
            my $rs = $dbh->do($sql, undef, $cid);
            if (!defined $rs || $rs == 0) {
                Mojo::Log->new->warn("DELETE FROM $table failed. cid is $cid.");
                $error = 1;
            }
        }
        # delete from firehose
        my $firehose = $self->new_instance_of("Firehose");
        $firehose->hard_delete("comment", $cid);
    }


    if ($error) {
        $self->rollback;
        return;
    }
    $self->commit;
    return 1;

    # delete firehose item
    # delete firehose_text item
    # delete firehose_topics_rendererd item
    # delete globjs
    # delete stories item
    # delete story_param
    # delete story_text
    # delete story_topics_chosen
    # delete story_topics_rendered
    # delete tags

    return;
}

1;
