# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is reddit.
#
# The Original Developer is the Initial Developer.  The Initial Developer of
# the Original Code is reddit Inc.
#
# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit
# Inc. All Rights Reserved.
###############################################################################

from r2.lib.db import tdb_cassandra
from r2.lib.db.thing import NotFound
from r2.lib.errors import MessageError
from r2.lib.utils import tup, fetch_things2
from r2.lib.filters import websafe
from r2.lib.hooks import HookRegistrar
from r2.lib.log import log_text
from r2.models import Account, Message, Report, Subreddit
from r2.models.award import Award
from r2.models.gold import append_random_bottlecap_phrase, creddits_lock
from r2.models.token import AwardClaimToken
from r2.models.wiki import WikiPage

from _pylibmc import MemcachedError
from pylons import g, c, config
from pylons.i18n import _

from datetime import datetime, timedelta
from copy import copy

admintools_hooks = HookRegistrar()

class AdminTools(object):

    def spam(self, things, auto=True, moderator_banned=False,
             banner=None, date=None, train_spam=True, **kw):
        from r2.lib.db import queries

        all_things = tup(things)
        new_things = [x for x in all_things if not x._spam]

        Report.accept(all_things, True)

        for t in all_things:
            if getattr(t, "promoted", None) is not None:
                g.log.debug("Refusing to mark promotion %r as spam" % t)
                continue

            if not t._spam and train_spam:
                note = 'spam'
            elif not t._spam and not train_spam:
                note = 'remove not spam'
            elif t._spam and not train_spam:
                note = 'confirm spam'
            elif t._spam and train_spam:
                note = 'reinforce spam'

            t._spam = True

            if moderator_banned:
                t.verdict = 'mod-removed'
            elif not auto:
                t.verdict = 'admin-removed'

            ban_info = copy(getattr(t, 'ban_info', {}))
            if isinstance(banner, dict):
                ban_info['banner'] = banner[t._fullname]
            else:
                ban_info['banner'] = banner
            ban_info.update(auto=auto,
                            moderator_banned=moderator_banned,
                            banned_at=date or datetime.now(g.tz),
                            **kw)
            ban_info['note'] = note

            t.ban_info = ban_info
            t._commit()

        if not auto:
            self.author_spammer(new_things, True)
            self.set_last_sr_ban(new_things)

        queries.ban(all_things, filtered=auto)

    def unspam(self, things, moderator_unbanned=True, unbanner=None,
               train_spam=True, insert=True):
        from r2.lib.db import queries

        things = tup(things)

        # We want to make unban-all moderately efficient, so when
        # mass-unbanning, we're going to skip the code below on links that
        # are already not banned.  However, when someone manually clicks
        # "approve" on an unbanned link, and there's just one, we want do
        # want to run the code below. That way, the little green checkmark
        # will have the right mouseover details, the reports will be
        # cleared, etc.

        if len(things) > 1:
            things = [x for x in things if x._spam]

        Report.accept(things, False)
        for t in things:
            ban_info = copy(getattr(t, 'ban_info', {}))
            ban_info['unbanned_at'] = datetime.now(g.tz)
            if unbanner:
                ban_info['unbanner'] = unbanner
            if ban_info.get('reset_used', None) == None:
                ban_info['reset_used'] = False
            else:
                ban_info['reset_used'] = True
            t.ban_info = ban_info
            t._spam = False
            if moderator_unbanned:
                t.verdict = 'mod-approved'
            else:
                t.verdict = 'admin-approved'
            t._commit()

        self.author_spammer(things, False)
        self.set_last_sr_ban(things)
        queries.unban(things, insert)
    
    def report(self, thing):
        pass

    def author_spammer(self, things, spam):
        """incr/decr the 'spammer' field for the author of every
           passed thing"""
        by_aid = {}
        for thing in things:
            if (hasattr(thing, 'author_id')
                and not getattr(thing, 'ban_info', {}).get('auto',True)):
                # only decrement 'spammer' for items that were not
                # autobanned
                by_aid.setdefault(thing.author_id, []).append(thing)

        if by_aid:
            authors = Account._byID(by_aid.keys(), data=True, return_dict=True)

            for aid, author_things in by_aid.iteritems():
                author = authors[aid]
                author._incr('spammer', len(author_things) if spam else -len(author_things))

    def set_last_sr_ban(self, things):
        by_srid = {}
        for thing in things:
            if getattr(thing, 'sr_id', None) is not None:
                by_srid.setdefault(thing.sr_id, []).append(thing)

        if by_srid:
            srs = Subreddit._byID(by_srid.keys(), data=True, return_dict=True)
            for sr_id, sr_things in by_srid.iteritems():
                sr = srs[sr_id]

                sr.last_mod_action = datetime.now(g.tz)
                sr._commit()
                sr._incr('mod_actions', len(sr_things))

    def adjust_gold_expiration(self, account, days=0, months=0, years=0):
        now = datetime.now(g.display_tz)
        if months % 12 == 0:
            years += months / 12
        else:
            days += months * 31
        days += years * 366

        existing_expiration = getattr(account, "gold_expiration", None)
        if existing_expiration is None or existing_expiration < now:
            existing_expiration = now
        account.gold_expiration = existing_expiration + timedelta(days)
        
        if account.gold_expiration > now and not account.gold:
            self.engolden(account)
        elif account.gold_expiration <= now and account.gold:
            self.degolden(account)

        account._commit()     

    def engolden(self, account):
        now = datetime.now(g.display_tz)
        account.gold = True
        description = "Since " + now.strftime("%B %Y")
        
        trophy = Award.give_if_needed("reddit_gold", account,
                                     description=description,
                                     url="/gold/about")
        if trophy and trophy.description.endswith("Member Emeritus"):
            trophy.description = description
            trophy._commit()

        account._commit()
        account.friend_rels_cache(_update=True)

    def degolden(self, account):
        Award.take_away("reddit_gold", account)
        account.gold = False
        account._commit()

    def admin_list(self):
        return list(g.admins)

    def create_award_claim_code(self, unique_award_id, award_codename,
                                description, url):
        '''Create a one-time-use claim URL for a user to claim a trophy.

        `unique_award_id` - A string that uniquely identifies the kind of
                            Trophy the user would be claiming.
                            See: token.py:AwardClaimToken.uid
        `award_codename` - The codename of the Award the user will claim
        `description` - The description the Trophy will receive
        `url` - The URL the Trophy will receive

        '''
        award = Award._by_codename(award_codename)
        token = AwardClaimToken._new(unique_award_id, award, description, url)
        return token.confirm_url()

admintools = AdminTools()

def cancel_subscription(subscr_id):
    q = Account._query(Account.c.gold_subscr_id == subscr_id, data=True)
    l = list(q)
    if len(l) != 1:
        g.log.warning("Found %d matches for canceled subscription %s"
                      % (len(l), subscr_id))
    for account in l:
        account.gold_subscr_id = None
        account._commit()
        g.log.info("%s canceled their recurring subscription %s" %
                   (account.name, subscr_id))

def all_gold_users():
    q = Account._query(Account.c.gold == True, Account.c._spam == (True, False),
                       data=True, sort="_id")
    return fetch_things2(q)

def accountid_from_paypalsubscription(subscr_id):
    if subscr_id is None:
        return None

    q = Account._query(Account.c.gold_subscr_id == subscr_id,
                       Account.c._spam == (True, False),
                       Account.c._deleted == (True, False), data=False)
    l = list(q)
    if l:
        return l[0]._id
    else:
        return None

def update_gold_users():
    now = datetime.now(g.display_tz)
    warning_days = 3
    renew_msg = _("[Click here for details on how to set up an "
                  "automatically-renewing subscription or to renew.]"
                  "(/gold) If you have any thoughts, complaints, "
                  "rants, suggestions about reddit gold, please write "
                  "to us at %(gold_email)s. Your feedback would be "
                  "much appreciated.\n\nThank you for your past "
                  "patronage.") % {'gold_email': g.goldthanks_email}

    for account in all_gold_users():
        days_left = (account.gold_expiration - now).days
        if days_left < 0:
            if account.pref_creddit_autorenew:
                with creddits_lock(account):
                    if account.gold_creddits > 0:
                        admintools.adjust_gold_expiration(account, days=31)
                        account.gold_creddits -= 1
                        account._commit()
                        continue

            admintools.degolden(account)

            subject = _("Your reddit gold subscription has expired.")
            message = _("Your subscription to reddit gold has expired.")
            message += "\n\n" + renew_msg
            message = append_random_bottlecap_phrase(message)

            send_system_message(account, subject, message,
                                distinguished='gold-auto')
        elif days_left <= warning_days and not account.gold_will_autorenew:
            hc_key = "gold_expiration_notice-" + account.name
            already_warned = g.hardcache.get(hc_key)
            if not already_warned:
                g.hardcache.set(hc_key, True, 86400 * (warning_days + 1))
                
                subject = _("Your reddit gold subscription is about to "
                            "expire!")
                message = _("Your subscription to reddit gold will be "
                            "expiring soon.")
                message += "\n\n" + renew_msg
                message = append_random_bottlecap_phrase(message)

                send_system_message(account, subject, message,
                                    distinguished='gold-auto')


def admin_ratelimit(user):
    return True

def is_banned_domain(dom):
    return None

def is_shamed_domain(dom):
    return False, None, None

def bans_for_domain_parts(dom):
    return []

def valid_thing(v, karma, *a, **kw):
    return not v._thing1._spam

def valid_user(v, sr, karma, *a, **kw):
    return True

def apply_updates(user):
    pass

def update_score(obj, up_change, down_change, vote, old_valid_thing):
     obj._incr('_ups',   up_change)
     obj._incr('_downs', down_change)


def _is_controversial(w, item):
    """Determine if an item meets all criteria to display as controversial."""
    cfg = g.live_config
    votes = max(item._ups + item._downs, 1)

    # A sample-size threshold before posts can be considered controversial
    if votes < cfg['cflag_min_votes']:
        return False

    # If an item falls within a boundary of upvote ratios, it's controversial
    # e.g. 0.4 < x < 0.6
    if cfg['cflag_lower_bound'] <= w.upvote_ratio <= cfg['cflag_upper_bound']:
        return True

    return False


def compute_votes(wrapper, item):
    wrapper.upvotes   = item._ups
    wrapper.downvotes = item._downs
    total_votes = max(item._ups + item._downs, 1)
    wrapper.upvote_ratio = float(item._ups) / total_votes
    wrapper.is_controversial = (c.user.pref_highlight_controversial and
                                _is_controversial(wrapper, item))

def ip_span(ip):
    ip = websafe(ip)
    return '<!-- %s -->' % ip

def filter_quotas(unfiltered):
    now = datetime.now(g.tz)

    baskets = {
        'hour':  [],
        'day':   [],
        'week':  [],
        'month': [],
        }

    new_quotas = []
    quotas_changed = False

    for item in unfiltered:
        delta = now - item._date

        age = delta.days * 86400 + delta.seconds

        # First, select a basket or abort if item is too old
        if age < 3600:
            basket = 'hour'
        elif age < 86400:
            basket = 'day'
        elif age < 7 * 86400:
            basket = 'week'
        elif age < 30 * 86400:
            basket = 'month'
        else:
            quotas_changed = True
            continue

        verdict = getattr(item, "verdict", None)
        approved = verdict and verdict in (
            'admin-approved', 'mod-approved')

        # Then, make sure it's worthy of quota-clogging
        if item._spam:
            pass
        elif item._score <= 0:
            pass
        elif age < 86400 and item._score <= g.QUOTA_THRESHOLD and not approved:
            pass
        else:
            quotas_changed = True
            continue

        baskets[basket].append(item)
        new_quotas.append(item._fullname)

    if quotas_changed:
        return baskets, new_quotas
    else:
        return baskets, None


def wiki_template(template_slug, sr=None):
    """Pull content from a subreddit's wiki page for internal use."""
    if not sr:
        try:
            sr = Subreddit._by_name(g.default_sr)
        except NotFound:
            return None

    try:
        wiki = WikiPage.get(sr, "templates/%s" % template_slug)
    except tdb_cassandra.NotFound:
        return None

    return wiki._get("content")


@admintools_hooks.on("account.registered")
def send_welcome_message(user):
    welcome_title = wiki_template("welcome_title")
    welcome_message = wiki_template("welcome_message")

    if not welcome_title or not welcome_message:
        g.log.warning("Unable to send welcome message: invalid wiki templates.")
        return

    welcome_title = welcome_title.format(username=user.name)
    welcome_message = welcome_message.format(username=user.name)

    return send_system_message(user, welcome_title, welcome_message)


def send_system_message(user, subject, body, system_user=None,
                        distinguished='admin', repliable=False,
                        add_to_sent=True, author=None):
    from r2.lib.db import queries

    if system_user is None:
        system_user = Account.system_user()
    if not system_user:
        g.log.warning("Can't send system message "
                      "- invalid system_user or g.system_user setting")
        return
    if not author:
        author = system_user

    item, inbox_rel = Message._new(author, user, subject, body,
                                   ip='0.0.0.0')
    item.distinguished = distinguished
    item.repliable = repliable
    item.display_author = system_user._id
    item._commit()

    try:
        queries.new_message(item, inbox_rel, add_to_sent=add_to_sent)
    except MemcachedError:
        raise MessageError('reddit_inbox')


if config['r2.import_private']:
    from r2admin.models.admintools import *
