# -*- coding: utf-8 -*-
from django.db import models
from django.template.defaultfilters import slugify
from django.conf import settings
from django.core.cache import cache

import datetime, json, os.path
from dateutil.relativedelta import relativedelta
from jsonfield import JSONField

from common import enum
from person.types import Gender, RoleType, SenatorClass, SenatorRank
from .name import get_person_name

from us import stateapportionment, get_congress_dates, statenames, get_congress_from_date, get_all_sessions

import functools
import collections
def cache_result(f):
    @functools.wraps(f)
    def g(self):
        if hasattr(self, "role"): return f(self)
        ckey = "cache_result_%s_%s_%d" % (self.__class__.__name__, f.__name__, self.id)
        v = cache.get(ckey)
        if not v:
            v = f(self)
            cache.set(ckey, v)
        return v
    return g

class Person(models.Model):
    """Members of Congress, Presidents, and Vice Presidents since the founding of the nation."""
	
    firstname = models.CharField(max_length=255, help_text="The person's first name or first initial.")
    lastname = models.CharField(max_length=255, help_text="The person's last name.")
    middlename = models.CharField(max_length=255, blank=True, help_text="The person's middle name (optional).")

    # misc
    birthday = models.DateField(blank=True, null=True, help_text="The person's birthday.")
    gender = models.IntegerField(choices=Gender, blank=True, null=True, help_text="The person's gender, if known. For historical data, the gender is sometimes not known.")
    
    # namemod set(['II', 'Jr.', 'Sr.', 'III', 'IV'])
    namemod = models.CharField(max_length=10, blank=True, help_text="The suffix on the person's name usually one of Jr., Sr., I, II, etc.")
    nickname = models.CharField(max_length=255, blank=True, help_text="The person's nickname. If set, the nickname should usually be displayed in quotes where a middle name would go. For instance, Joe \"Buster\" Smith.")

    # links
    bioguideid = models.CharField(max_length=255, blank=True, null=True, help_text="The person's ID on bioguide.congress.gov. May be null if the person served only as a president and not in Congress.")
    pvsid = models.CharField(max_length=255, blank=True, null=True, help_text="The person's ID on vote-smart.org (Project Vote Smart), if known.")
    osid = models.CharField(max_length=255, blank=True, null=True, help_text="The person's ID on opensecrets.org (The Center for Responsive Politics), if known.")
    youtubeid = models.CharField(max_length=255, blank=True, null=True, help_text="The name of the person's official YouTube channel, if known.")
    twitterid = models.CharField(max_length=50, blank=True, null=True, help_text="The name of the person's official Twitter handle, if known.")
    cspanid = models.IntegerField(blank=True, null=True, help_text="The ID of the person on CSPAN websites, if known.")
    fediverse_webfinger = JSONField(blank=True, null=True, help_text="The person's official (not campaign) Mastodon account's webfinger record, or a stub.")
    
    # cached name info
    name = models.CharField(max_length=96, help_text="The person's full name with title, district, and party information for current Members of Congress, in a typical display format.")
    sortname = models.CharField(max_length=64, help_text="The person's name suitable for sorting lexicographically by last name or for display in a sorted list of names. Title, district, and party information are included for current Members of Congress.")
    

    # indexing
    def get_index_text(self):
        # We need to index the name, and also the name without
        # hard-to-type characters.
        def str2(s): return s if s != None else ""
        import unicodedata
        n = self.name_no_details().replace("\u201c", " ").replace("\u201d", " ")
        r = n + "\n" + \
            "".join(c for c in unicodedata.normalize('NFKD', n) if not unicodedata.combining(c)) + "\n"
        most_recent_role = self.get_most_recent_role()
        if most_recent_role:
            r += str2(most_recent_role.state) + " " + str2(statenames.get(most_recent_role.state))
        return r
    def get_index_text_boosted(self):
        return self.lastname
    haystack_index = ('gender',)
    haystack_index_extra = (
        ('sortname_strxfrm>sortname', 'Char'),
        ('is_currently_serving', 'Boolean'),
        ('current_role_type', 'Integer'), ('current_role_title', 'Char'), ('all_role_types', 'MultiValue'),
        ('current_role_state', 'Char'), ('all_role_states', 'MultiValue'),
        ('current_role_district', 'Integer'), ('all_role_districts', 'MultiValue'),
        ('current_role_party', 'Char'), ('all_role_parties', 'MultiValue'),
        ('first_took_office', 'Date'), ('left_office', 'Date'))
    @property
    def sortname_strxfrm(self):
        # The default sort order of Python and Haystack sorts á after z,
        # which puts legislators into the wrong order. Before going into
        # the search index, expand the name to a binary encoding that
        # sorts correctly.
        import locale
        locale.setlocale(locale.LC_ALL, "en_US.UTF-8")
        return locale.strxfrm(self.sortname)
    def get_current_role_field(self, fieldname):
        # Returns the value of a field on a current PersonRole for
        # this Person. If the Person has no current role, returns None.
        role = self.get_current_role()
        if not role: return None
        ret = getattr(role, fieldname)
        if callable(ret): ret = ret()
        return ret
    def current_role_type(self):
        return self.get_current_role_field('role_type')
    def current_role_title(self):
        return self.get_current_role_field('get_title')
    def all_role_types(self):
        return sorted(set(self.roles.values_list("role_type", flat=True)))
    def current_role_state(self):
        return self.get_current_role_field('state')
    def all_role_states(self):
        return sorted(set(self.roles.values_list("state", flat=True)))
    def current_role_district(self):
        return self.get_current_role_field('district')
    def all_role_districts(self):
        # combine the state and district so that filtering on
        # state=A district=X doesn't return a Person with values
        # [(state=A, district=Y), (state=B, district=X)].
        # Exclude districts that are empty (None, i.e. for senators
        # presidents, etc.) or unknown (-1).
        return sorted(set("%s-%02d" % sd for sd in self.roles.values_list("state", "district")
            if sd[1] not in (None, -1)))
    def current_role_party(self):
        return self.get_current_role_field('party')
    def all_role_parties(self):
        return sorted(set(self.roles.values_list("party", flat=True)), key=lambda x : (str(type(x)), x)) # allow None's to be sorted with strings
    def first_took_office(self):
        # first took office for the most recent role
        role = self.get_most_recent_role()
        if not role: return None
        return role.logical_dates()[0]
    def left_office(self):
        # term end date for the most recent role
        role = self.get_most_recent_role()
        if not role: return None
        return role.enddate

    #######
    # api
    api_recurse_on_single = ('roles', 'committeeassignments')
    api_additional_fields = {
        "link": lambda obj : settings.SITE_ROOT_URL + obj.get_absolute_url(),
    }
    api_example_id = 400326
    #######

    def __str__(self):
        return self.name

    @staticmethod
    def from_state_and_district(state, district):
        qs = PersonRole.objects.filter(current=True).select_related("person")
        qs = qs.filter(role_type=RoleType.representative, state=state, district=district) \
            | qs.filter(role_type=RoleType.senator, state=state)
        ret = []
        for role in qs:
            person = role.person
            person.role = role
            ret.append(person)
        ret.sort(key = lambda person : (person.role.get_sort_key(), person.sortname_strxfrm))
        return ret

    @property
    def fullname(self):
        return '%s %s' % (self.firstname, self.lastname)

    @cache_result
    def name_no_district(self):
        return get_person_name(self, firstname_position='before', role_recent=True, show_district=False)

    @cache_result
    def name_no_details(self):
        """The person's full name (excluding all title details)."""
        return get_person_name(self, firstname_position='before')
        
    @cache_result
    def name_no_details_lastfirst(self):
        return get_person_name(self, firstname_position='after')
            
    @cache_result
    def name_lastfirst_short(self):
        return get_person_name(self, firstname_position='after', firstname_style="nickname")
            
    @cache_result
    def name_and_title(self):
        return get_person_name(self, firstname_position='before', role_recent=True, show_party=False, show_district=False)

    @cache_result
    def name_lastonly(self):
        return get_person_name(self, firstname_position='none', role_recent=True, show_party=True, show_district=True)

    def set_names(self):
        self.sortname = get_person_name(self, firstname_position='after', role_recent=True, show_district=True, show_title=False, show_type=True)
        self.name = get_person_name(self, firstname_position='before', role_recent=True)

    @staticmethod
    def UpdateNames():
        # After this you probably should run ./manage.py update_index -v 0 -u person person.
        for p in Person.objects.all():
          prev = (p.name, p.sortname)
          p.set_names()
          if (p.name, p.sortname) != prev:
              print(p.id, prev, "=>", (p.name, p.sortname))
              p.save(update_fields=["name", "sortname"])

    @property
    def him_her(self):
        return { Gender.male: "him", Gender.female: "her" }.get(self.gender, "them")
    @property
    def his_her(self):
        return { Gender.male: "his", Gender.female: "her" }.get(self.gender, "their")
    @property
    def he_she(self):
        return { Gender.male: "he", Gender.female: "she" }.get(self.gender, "they")
    @property
    def he_she_cap(self):
        return self.he_she[0].upper() + self.he_she[1:]
        
    @property
    def current_role(self):
        return self.get_current_role()
    def get_current_role(self):
        try:
            if not self.roles.all()._result_cache: # if this has already been feteched by prefetch_related
                return self.roles.get(current=True)
            else:
                return [r for r in self.roles.all() if r.current][0]
        except (PersonRole.DoesNotExist, IndexError):
            return None
    def is_currently_serving(self):
        return self.roles.filter(current=True).exists()

    def get_absolute_url(self):
        name = slugify('%s %s' % (self.firstname if not self.firstname.endswith(".") else self.middlename, self.lastname))
        name = name.replace('-', '_')
        return '/congress/members/%s/%d' % (name, self.pk)

    def get_age(self):
        if not self.birthday:
            return 0
        else:
            today = datetime.date.today()
            return relativedelta(today, self.birthday).years

    def roles_condensed(self, round_end=True):
        ret = []
        for role in self.roles.order_by('startdate'):
            role.id = None # prevent corruption

            # since we'll end up only showing years, round Jan 3 back to the previous year
            role.enddate = PersonRole.round_down_enddate(role.enddate, do_rounding=round_end)

            # is the end a special election?
            role.ends_with_special_election = ((role.extra or {}).get("end-type") == "special-election")

            if len(ret) > 0 and role.continues_from(ret[-1]):
                # condense party_affiliations
                import copy
                prev_parties = copy.deepcopy((ret[-1].extra or {}).get("party_affiliations", [])) # clone
                next_parties = list((role.extra or {}).get("party_affiliations", [])) # clone
                if len(prev_parties) == 0 and len(next_parties) == 0 and ret[-1].party == role.party and ret[-1].caucus == role.caucus:
                    # Neither have party_affiliations set *and* the party is the same
                    # for the two roles, so merge without anything complicated.
                    pass
                else:
                    # One or both has party_affiliations, so merge. If either has
                    # none, fill it in with a dummy entry and merge the last entry
                    # of the previous with the first one of the next.
                    if len(prev_parties) == 0: prev_parties = [{ "start": ret[-1].startdate.isoformat(), "end": ret[-1].enddate.isoformat(), "party": ret[-1].party, "caucus": ret[-1].caucus }]
                    if len(next_parties) == 0: next_parties = [{ "start": role.startdate.isoformat(), "end": role.enddate.isoformat(), "party": role.party, "caucus": role.caucus }]
                    if prev_parties[-1]["party"] == next_parties[0]["party"] and prev_parties[-1].get("caucus") == next_parties[0].get("caucus"):
                        prev_parties[-1]["end"] = next_parties[0]["end"]
                        next_parties.pop(0)
                    ret[-1].extra = copy.deepcopy(ret[-1].extra or {}) # initialize
                    ret[-1].extra["party_affiliations"] = prev_parties + next_parties

                # extend the end date
                ret[-1].enddate = role.enddate
                ret[-1].current |= role.current

                # the following are for the most recent value although they may change during a term
                ret[-1].party = role.party
                ret[-1].caucus = role.caucus
                ret[-1].senator_rank = role.senator_rank
                ret[-1].leadership_title = role.leadership_title
                ret[-1].ends_with_special_election = role.ends_with_special_election
            else:
                ret.append(role)
        ret.reverse()
        return ret

    def get_role_at_date(self, when, congress=None):
        if isinstance(when, datetime.datetime):
            when = when.date()
        
        # A person may have two roles on the same date, such as when simultaneously
        # resigning from the House to take office in the Senate. In that case, return the
        # most recent role.
        for r in self.roles.filter(startdate__lte=when, enddate__gte=when).order_by("-startdate"):
            if congress is not None and r.congress_numbers() is not None and congress not in r.congress_numbers(): continue
            return r
        return None

    def get_last_role_at_congress(self, congress):
        start, end = get_congress_dates(congress)
        try:
            return self.roles.filter(startdate__lte=end, enddate__gte=start).order_by('-startdate')[0]
        except IndexError:
            return None
            
    def get_role_at_year(self, year):
        try:
            return self.roles.filter(startdate__lte=("%d-12-31"%year), enddate__gte=("%d-01-01"%year)).order_by('-startdate')[0]
        except IndexError:
            return None
            
    _most_recent_role = None
    def get_most_recent_role(self):
        if self._most_recent_role: return self._most_recent_role
        try:
            if not self.roles.all()._result_cache: # if this has already been feteched by prefetch_related
                r = self.roles.order_by('-startdate')[0]
            else:
                r = sorted(self.roles.all(), key=lambda r : r.startdate, reverse=True)[0]
            self._most_recent_role = r
            return r
        except IndexError:
            return None
    def get_most_recent_congress_role(self, excl_trivial=False):
        for r in self.roles.filter(role_type__in=(RoleType.senator, RoleType.representative)).order_by('-startdate'):
            if excl_trivial and (r.enddate-r.startdate).days < 100: continue
            return r
        return None

    def get_photo_url(self, size=100):
        """Return URL of 100px photo, or other specified size."""
        return '/static/legislator-photos/%d-%dpx.jpeg' % (self.pk, size)
    def get_photo_url_50(self):
        return self.get_photo_url(size=50)
    def get_photo_url_100(self):
        return self.get_photo_url(size=100)
    def get_photo_url_200(self):
        return self.get_photo_url(size=200)
    def has_photo(self, size=100):
        import os.path
        return os.path.exists("." + self.get_photo_url(size=size))

    class Meta:
        pass # ordering = ['lastname', 'firstname'] # causes prefetch related to be slow

    def vote_sources(self):
        from vote.models import Vote
        sources = set()
        for v in Vote.objects.filter(voters__person=self).order_by().values("source").distinct():
            if v["source"] in (1, 2):
                sources.add("congress")
            elif v["source"] == 3:
                sources.add("keithpoole")
        return sources

    def get_photo(self):
        size = 200
        photo_path = 'static/legislator-photos/%d-%dpx.jpeg' % (self.pk, size)
        if os.path.exists(photo_path):
            photo_url = '/' + photo_path
            with open(photo_path.replace("-%dpx.jpeg" % size, "-credit.txt"), "r") as f:
                photo_credit = f.read().strip().split(" ", 1)
                return photo_url, photo_credit
        else:
            return None, None

    @staticmethod
    def load_session_stats(session):
      # Which Congress is it?
        for congress, s, sd, ed in get_all_sessions():
            if s == session: break # leaves "congress" variable set
        else:
            raise ValueError("Invalid session: %s" % session)

        fn = "data/analysis/by-congress/%d/session-%s.json" % (congress, session)
        try:
            datafile = json.load(open(fn))
            datafile["meta"]["pub_year"] = session
            if datafile["meta"]["is_full_congress_stats"]:
                datafile["meta"]["startdate"] = get_congress_dates(congress)[0]
                datafile["meta"]["enddate"] = get_congress_dates(congress)[1]
            else:
                datafile["meta"]["startdate"] = sd
                datafile["meta"]["enddate"] = ed
        except IOError:
            raise ValueError("No statistics are available for session %s." % session)

        return datafile

    def get_session_stats(self, session):
        datafile = Person.load_session_stats(session)
  
        if str(self.id) not in datafile["people"]:
            raise ValueError("No statistics available for person %d in session %s." % (self.id, session))

        stats = datafile["people"][str(self.id)]
        stats["meta"] = datafile["meta"] # copy this over
        return stats

    def get_feed(self, feed_type="p"):
        if feed_type not in ("p", "pv", "ps"): raise ValueError(feed_type)
        from events.models import Feed
        return Feed.objects.get_or_create(feedname="%s:%d" % (feed_type, self.id))[0]

    @staticmethod
    def from_feed(feed):
        if ":" not in feed.feedname or feed.feedname.split(":")[0] not in ("p", "pv", "ps"): raise ValueError(feed.feedname)
        pid = int(feed.feedname.split(":")[1])
        cache_key = "person:%d" % pid
        p = cache.get(cache_key)
        if not p:
            p = Person.objects.get(id=pid)
            cache.set(cache_key, p, 60*60*4) # 4 hours
        return p

    def get_mastodon_handle_and_link(self):
        # TODO: Validate the 'href' value in some way?
        if not self.fediverse_webfinger: return None
        handle = self.fediverse_webfinger.get("subject")
        if not isinstance(handle, str) or not handle.startswith("acct:"): return None
        handle = "@" + handle[5:]
        for link in self.fediverse_webfinger.get("links", []):
            if link.get('rel') == 'http://webfinger.net/rel/profile-page' \
              and link['href'].startswith("https://"):
                return handle, link['href']
        for link in self.fediverse_webfinger.get("links", []):
            if link.get('type') == 'text/html' \
              and link['href'].startswith("https://"):
                return handle, link['href']
        return None # could not find link

    def set_mastodon_handle(self, mastodon_id, force_refresh=False):
        # Clear info if no account is set.
        if not mastodon_id:
            #if self.fediverse_webfinger: print("Clearing", self)
            self.fediverse_webfinger = None
            return

        # Parse ID to local and instance parts and form an acct: URI.
        import re
        m = re.match(r"^@(.*)@(.*)$", mastodon_id)
        if not m: raise ValueError(mastodon_id)
        local_name, instance_name = m.groups()
        acct = "acct:" + local_name + "@" + instance_name

        # If force_refresh is False and there is already recent webfinger
        # information for this account stored, then no update is needed.
        # Since the stored "subject" may be normalized from the handle
        # we have, check against the original handle we queried.
        # TODO: Check __fetched_date.
        if isinstance(self.fediverse_webfinger, dict) \
           and self.fediverse_webfinger.get("__query_resource") == acct:
           #and not force_refresh:
           #print("No change for", self)
           return

        # Issue a webfinger request.
        import requests
        import urllib.parse
        url = 'https://{}/.well-known/webfinger?resource={}'.format(
            urllib.parse.quote(instance_name),
            urllib.parse.quote(acct),
        )
        print(url + "...")
        resp = requests.get(url=url)
        data = resp.json()

        # Attach the handle we queried on and the date so we can refresh
        # the info after an interval.
        data["__query_resource"] = acct
        data["__fetched_date"] = datetime.datetime.now().isoformat()

        # Store webfinger response.
        self.fediverse_webfinger = data

class PersonRole(models.Model):
    """Terms held in office by Members of Congress, Presidents, and Vice Presidents. Each term corresponds with an election, meaning each term in the House covers two years (one 'Congress'), as President/Vice President four years, and in the Senate six years (three 'Congresses')."""
	
    person = models.ForeignKey('person.Person', related_name='roles', on_delete=models.CASCADE)
    role_type = models.IntegerField(choices=RoleType, db_index=True, help_text="The type of this role: a U.S. senator, a U.S. congressperson, a U.S. president, or a U.S. vice president.")
    current = models.BooleanField(default=False, choices=[(False, "No"), (True, "Yes")], db_index=True, help_text="Whether the role is currently held, or if this is archival information.")
    startdate = models.DateField(db_index=True, help_text="The date the role began (when the person took office).")
    enddate = models.DateField(db_index=True, help_text="The date the role ended (when the person resigned, died, etc.)")
    # http://en.wikipedia.org/wiki/Classes_of_United_States_Senators
    senator_class = models.IntegerField(choices=SenatorClass, blank=True, null=True, db_index=True, help_text="For senators, their election class, which determines which years they are up for election. (It has nothing to do with seniority.)") # None for representatives
    senator_rank = models.IntegerField(choices=SenatorRank, blank=True, null=True, help_text="For senators, their state rank, i.e. junior or senior. For historical data, this is their last known rank.") # None for representatives
    # http://en.wikipedia.org/wiki/List_of_United_States_congressional_districts
    district = models.IntegerField(blank=True, null=True, db_index=True, help_text="For representatives, the number of their congressional district. 0 for at-large districts, -1 in historical data if the district is not known.") # None for senators/presidents
    state = models.CharField(choices=sorted(statenames.items()), max_length=2, blank=True, db_index=True, help_text="For senators and representatives, the two-letter USPS abbrevation for the state or territory they are serving. Values are the abbreviations for the 50 states (each of which have at least one representative and two senators, assuming no vacancies) plus DC, PR, and the island territories AS, GU, MP, and VI (all of which have a non-voting delegate), and for really old historical data you will also find PI (Philippines, 1907-1946), DK (Dakota Territory, 1861-1889), and OR (Orleans Territory, 1806-1811) for non-voting delegates.")
    party = models.CharField(max_length=255, blank=True, null=True, db_index=True, help_text="The political party of the person. If the person changes party, it is usually the most recent party during this role.")
    caucus = models.CharField(max_length=255, blank=True, null=True, help_text="For independents, the party that the legislator caucuses with. If changed during a term, the most recent.")
    website = models.CharField(max_length=255, blank=True, help_text="The URL to the official website of the person during this role, if known.")
    phone = models.CharField(max_length=64, blank=True, null=True, help_text="The last known phone number of the DC congressional office during this role, if known.")
    leadership_title = models.CharField(max_length=255, blank=True, null=True, help_text="The last known leadership role held during this role, if any.")
    extra = JSONField(blank=True, null=True, help_text="Additional schema-less information stored with this object.")

    # API
    api_recurse_on = ('person',)
    api_additional_fields = {
        "title": "get_title_abbreviated",
        "title_long": "get_title",
        "description": "get_description",
        "congress_numbers": "congress_numbers",
    }
    api_example_parameters = { "current": "true", "sort": "state" }

    class Meta:
        pass # ordering = ['startdate'] # causes prefetch_related to be slow

    def __str__(self):
        return '[%s] %s / %s to %s / %s / %s' % (self.id, self.person.fullname, self.startdate, self.enddate, self.get_role_type_display(), ",".join(str(c) for c in (self.congress_numbers() or [])))

    def get_office_id(self):
        if self.role_type in (RoleType.president, RoleType.vicepresident):
            return RoleType.by_value(self.role_type).key
        if self.role_type == RoleType.senator:
            return ("sen", self.state, self.senator_class)
        if self.role_type == RoleType.representative:
            return ("rep", self.state, self.district)
        raise ValueError()
       
    def continues_from(self, prev):
        if self.startdate - prev.enddate > datetime.timedelta(days=120): return False
        if self.role_type != prev.role_type: return False
        if self.senator_class != prev.senator_class: return False
        if self.state != prev.state: return False
        if self.district != prev.district: return False
        return True

    def get_title(self):
        """The long form of the title used to prefix the names of people with this role: Representative, Senator, President, or Resident Commissioner."""
        return self.get_title_name(short=False)

    def get_title_abbreviated(self):
        """The title used to prefix the names of people with this role: Rep., Sen., President, Del. (delegate), or Commish. (resident commissioner)."""
        return self.get_title_name(short=True)

    def get_title_name(self, short):
        if self.role_type == RoleType.president:
            return 'President' if short else 'President of the United States'
        if self.role_type == RoleType.vicepresident:
            return 'Vice President' if short else 'Vice President of the United States'
        if self.role_type == RoleType.senator:
            return 'Sen.' if short else 'Senator'
        if self.role_type == RoleType.representative:
            if self.state not in stateapportionment:
                # All of the former 'states' were territories that sent delegates.
                return 'Rep.' if short else 'Representative'
            if self.state == 'PR':
                return 'Commish.' if short else 'Resident Commissioner'
            if stateapportionment[self.state] == 'T':
                # These folks are also commonly called delegates, but out of respect
                # for their disenfranchised constituents we refer to them as representatives.
                return 'Rep.' if short else 'Representative'
            return 'Rep.' if short else 'Representative'
            
    def state_name(self):
        if not self.state: return "the United States"
        return statenames[self.state]

    def state_name_article(self):
        if not self.state: return "the United States"
        ret = statenames[self.state]
        if self.state in ("DC", "MP", "VI", "PI", "OL"):
            ret = "the " + ret
        return ret
            
    def get_description(self):
        """A description of this role, e.g. Delegate for District of Columbia At Large."""
        
        from django.contrib.humanize.templatetags.humanize import ordinal
        
        if self.role_type in (RoleType.president, RoleType.vicepresident):
            return self.get_title_name(False)
        if self.role_type == RoleType.senator:
            js = ""
            if self.current and self.senator_rank: js = self.get_senator_rank_display() + " "
            return js + self.get_title_name(False) + " for " + statenames[self.state]
        if self.role_type == RoleType.representative:
            if self.district == -1 or stateapportionment.get(self.state) in ("T", None): # unknown district / current territories and former state-things, all of which send/sent delegates
                return self.get_title_name(False) + " for " + self.state_name_article()
            elif self.district == 0:
                return self.get_title_name(False) + " for " + statenames[self.state] + " At Large"
            else:
                return self.get_title_name(False) + " for " + statenames[self.state] + "'s " + ordinal(self.district) + " congressional district"

    def get_description_natural(self):
        """A description in HTML of this role in sentence form, e.g. the delegate for the District of Columbia's at-large district."""
        
        from website.templatetags.govtrack_utils import ordinalhtml

        (statename, statename_article) = (self.state_name_article(), "")
        if statename.startswith("the "): (statename, statename_article) = (statename[4:], "the ")
        statename = '%s<a href="/congress/members/%s">%s</a>' % (statename_article, self.state, statename)
        
        if self.role_type in (RoleType.president, RoleType.vicepresident):
            return self.get_title_name(False)
        if self.role_type == RoleType.senator:
            js = "a "
            if self.current and self.senator_rank: js = "the " + self.get_senator_rank_display().lower() + " "
            return js + "senator from " + statename
        if self.role_type == RoleType.representative:
            if stateapportionment.get(self.state) in ("T", None): # current territories and former state-things, all of which send/sent delegates
                return "the %s from %s" % (
                    self.get_title_name(False).lower(),
                    statename
                )
            else:
                if self.district == -1:
                    return "the representative for " + statename
                elif self.district == 0:
                    return "the representative for " + statename + "\u2019s at-large district"
                else:
                    return "the representative for " + statename + "\u2019s " + ordinalhtml(self.district) + " congressional district"

    def congress_numbers(self):
        """The Congressional sessions (Congress numbers) that this role spans, as a list from the starting Congress number through consecutive numbers to the ending Congress number."""
        # Senators can span Congresses, so return a range.
        c1 = get_congress_from_date(self.startdate, range_type="start")
        c2 = get_congress_from_date(self.enddate, range_type="end")
        if not c1 or not c2: return None
        return list(range(c1, c2+1)) # congress number only, not session

    def most_recent_congress_number(self):
        n = self.congress_numbers()
        if not n: return None
        n = n[-1]
        if n > settings.CURRENT_CONGRESS: n = settings.CURRENT_CONGRESS # we don't ever mean to ask for a future one (senators, PR res com)
        return n

    def get_party(self):
        # If the person didn't change parties, just return the party.
        # Otherwise return "most recently a PARTY1 (year1-year2) and before that (year3-4), and ..."
        if self.party is None: return "(unknown party)"
        from parser.processor import Processor
        parties = (self.extra or {}).get("party_affiliations", [])
        def a_an(word): return "a" if word[0].lower() not in "aeiou" else "an"
        if len(parties) <= 1:
            return a_an(self.party) + " " + self.party + ("" if not self.caucus else " caucusing with the " + self.caucus + "s")
        parties = [
            "%s %s%s (%d-%s)" % (
                a_an(entry["party"]),
                entry["party"],
                "" if not entry.get("caucus") else " caucusing with the " + entry["caucus"] + "s",
                Processor.parse_datetime(entry["start"]).year,
                "" if self.current and i == len(parties)-1
                else PersonRole.round_down_enddate(Processor.parse_datetime(entry["end"])).year,
            )
            for i, entry in enumerate(parties)
            ]
        if self.current:
            most_recent_descr1 = ""
            most_recent_descr2 = ", "
        else:
            most_recent_descr1 = "most recently "
            most_recent_descr2 = " and "
        most_recent = parties.pop(-1)
        if len(parties) > 1: parties[-1] = "and " + parties[-1]
        return most_recent_descr1 + most_recent + most_recent_descr2 + "previously " + (", " if len(parties) > 2 else " ").join(parties)

    def get_party_on_date(self, when, caucus=False):
        if isinstance(when, datetime.datetime):
            when = when.date()
        if self.extra and "party_affiliations" in self.extra:
            for pa in self.extra["party_affiliations"]:
                if pa['start'] <= when.isoformat() <= pa['end']:
                    if caucus: return pa.get("caucus") or pa['party']
                    return pa['party']
        if caucus: return self.caucus or self.party
        return self.party

    @property
    def is_territory(self):
        # a current territory
        return stateapportionment.get(self.state) == "T"

    @property
    def is_historical_territory(self):
        # a historical territory
        # note: self.state is "" for presidents/vps
        return self.state and stateapportionment.get(self.state) is None

    def create_events(self, prev_role, next_role):
        now = datetime.datetime.now().date()
        from events.models import Feed, Event
        with Event.update(self) as E:
            f = self.person.get_feed()
            if not prev_role or not self.continues_from(prev_role):
                E.add("termstart", self.startdate, f)
            if not next_role or not next_role.continues_from(self):
                if self.enddate <= now: # because we're not sure of end date until it happens
                    E.add("termend", self.enddate, f)
        
    def render_event(self, eventid, feeds):
        self.person.role = self # affects name generation
        return {
            "type": "Elections and Offices",
            "date_has_no_time": True,
            "date": self.startdate if eventid == "termstart" else self.enddate,
            "title": self.person.name + (" takes office as " if eventid == "termstart" else " leaves office as ") + self.get_description(),
            "url": self.person.get_absolute_url(),
            "body_text_template": "{{name}} {{verb}} {{term}}.",
            "body_html_template": "<p>{{name}} {{verb}} {{term}}.</p>",
            "context": {
                "name": self.person.name,
                "verb": ("takes office as" if eventid == "termstart" else "leaves office as"),
                "term": self.get_description(),
            }
            }

    def logical_dates(self, round_end=False):
        startdate = None
        enddate = None
        prev_role = None
        found_me = False
        for role in self.person.roles.filter(role_type=self.role_type, senator_class=self.senator_class, state=self.state, district=self.district).order_by('startdate'):
            if found_me and not role.continues_from(prev_role):
                break
            if prev_role == None or not role.continues_from(prev_role):
                startdate = role.startdate
            enddate = PersonRole.round_down_enddate(role.enddate, do_rounding=round_end)
            prev_role = role
            if role.id == self.id:
                found_me = True
        if not found_me: raise Exception("Didn't find myself?!")
        return (startdate, enddate)

    @staticmethod
    def round_down_enddate(d, do_rounding=True):
        # If a date ends in the first three days of January, round it down to
        # December 31 of the previou year, so that we can show a nice year for
        # term end dates (2011-2012 rather than 2011-2013 which misleadingly
        # implies it was more than a few days in 2013 that probably weren't
        # in session anyway).
        if do_rounding:
            if d.month == 1 and d.day <= 3:
                return datetime.date(d.year-1, 12, 31)
        return d

    def next_election_year(self):
        # For current terms, roles end at the end of a Congress on Jan 3.
        # The election occurs in the year before.
        #
        # EXCEPT: Senators appointed to fill a term may be up for re-election
        # by special election sooner than the term end date stored in our
        # data. The end date is thus not known because it will be when the
        # special election winner is certified.
        if not self.current: raise ValueError()
        if (self.extra or {}).get("end-type") == "special-election":
            return self.enddate.year
        return self.enddate.year-1

    def is_up_for_election(self):
        if not self.current: return False
        if settings.CURRENT_ELECTION_DATE is None: return False # no election cycle is current
        if settings.CURRENT_ELECTION_DATE < datetime.datetime.now().date(): return False # election is over
        return self.next_election_year() == settings.CURRENT_ELECTION_DATE.year # is up this cycle

    def did_election_just_happen(self):
        if not self.current: return False
        if settings.CURRENT_ELECTION_DATE is None: return False # no election cycle is current
        if settings.CURRENT_ELECTION_DATE > datetime.datetime.now().date(): return False # election hasn't happened yet
        return self.next_election_year() == settings.CURRENT_ELECTION_DATE.year # is up this cycle

    def get_most_recent_session_stats(self):
        # Which Congress and session's end date is the most recently covered by this role?
        errs = []
        congresses = self.congress_numbers()
        for congress, session, sd, ed in reversed(get_all_sessions()):
            if congress not in congresses: continue
            if self.startdate < ed <= self.enddate:
                try:
                    return self.person.get_session_stats(session)
                except ValueError as e:
                    errs.append(str(e))
        raise ValueError("No statistics are available for this role: %s" % "; ".join(errs))

    def opposing_party(self):
        if self.party == "Democrat": return "Republican"
        if self.party == "Republican": return "Democrat"
        return None

    def get_sort_key(self):
        # As it happens, our enums define a good sort order between senators and representatives.
        return (self.role_type, self.senator_rank)

    @staticmethod
    def get_majority_party(role_type, when):
        # Get the majority party for the Houe or Senate
        # (passing RoleType.repreentative or RoleType.senator)
        # on a particular date. The result is cached in memory
        # indefinitely.

        import dateutil.parser

        # Initialize the cache.
        if not hasattr(PersonRole.get_majority_party, 'cache'):
            PersonRole.get_majority_party.cache = { RoleType.senator: [], RoleType.representative: [] }

        # Search the cache for any matching time period.
        for start_date, end_date, majority_party in PersonRole.get_majority_party.cache[role_type]:
            if start_date <= when < end_date:
                return majority_party

        # Compute the majority party. Get all legislators serving
        # on the given date. Use "gt" and not "gte" on the end date
        # because at the ends of Congresses turnover is at noon, and
        # we'll assume the caller is asking for the next Congress.
        roles = list(PersonRole.objects.filter(role_type=role_type,
                                                startdate__lte=when,
                                                enddate__gt=when))

        # For legislators who changed party mid-term, update the role
        # party, caucus, start, and end fields accordingly.
        for p in roles:
            if p.extra and "party_affiliations" in p.extra:
                for pa in p.extra["party_affiliations"]:
                    if pa['start'] <= when.isoformat() < pa['end']:
                        p.party = pa['party']
                        p.caucus = pa.get('caucus')
                        p.startdate = dateutil.parser.parse(pa['start']).date()
                        p.enddate = dateutil.parser.parse(pa['end']).date()

        # Compute the counts by party (or caucus if set, for independents).
        from collections import defaultdict
        party_counts = defaultdict(lambda : 0)
        for p in roles:
            party_counts[p.caucus or p.party] += 1

        # Get the party with the highest count.
        majority_party = max(party_counts, key = lambda p : party_counts[p])

        # If the highest count is not a majority (i.e. a tie)...
        if party_counts[majority_party] <= len(roles) / 2:
            # In the House, there is no majority.
            majority_party = None

            # In the Senate, the majority is determined by the party of
            # the Vice President (although in reality there is likely
            # a complex power-sharing agreement). Use "gt" and not "gte"
            # on the end date because at the ends of presidential terms
            # turnover is at noon, and we'll assume the caller is asking
            # about the afternoon.
            vp = PersonRole.objects.filter(role_type=RoleType.vicepresident,
                                           startdate__lte=when,
                                           enddate__gt=when).first()
            if vp:
                majority_party = vp.caucus or vp.party
                roles.append(vp) # limit the cache entry to the duration of this role as well

        # This can be cached for the time period starting with the
        # beginning of the most recent role and ending with the nearest
        # end of a role, and not in the future.
        cache_start = max([p.startdate for p in roles])
        cache_end = min([p.enddate for p in roles] + [datetime.datetime.now().date()])

        # Additionally, in case there are other roles that start/end within
        # this window, we have to reduce it further.
        role = PersonRole.objects.filter(role_type=role_type, enddate__lt=when)\
                          .order_by('-enddate').first()
        if role: cache_start = max(cache_start, role.enddate)
        role = PersonRole.objects.filter(role_type=role_type, startdate__gt=when)\
                          .order_by('startdate').first()
        if role: cache_end = min(cache_end, role.startdate)

        # Add cache entry.
        PersonRole.get_majority_party.cache[role_type].append((cache_start, cache_end, majority_party))

        return majority_party

# Feeds

def feed_thumbnail_image_url(feed):
    p = Person.from_feed(feed)
    if not p.has_photo():
        return None
    return p.get_photo_url_200()

from events.models import Feed
Feed.register_feed(
    "p:",
    title = lambda feed : Person.from_feed(feed).name,
    noun = "legislator",
    includes = lambda feed : [Person.from_feed(feed).get_feed("pv"), Person.from_feed(feed).get_feed("ps")],
    link = lambda feed: Person.from_feed(feed).get_absolute_url(),
    scoped_title = lambda feed : "All Events for " + Person.from_feed(feed).lastname,
    category = "federal-other",
    description = "You will get updates about major activity on sponsored bills and how this Member of Congress votes in roll call votes.",
    is_subscribable = lambda feed : Person.from_feed(feed).get_current_role() is not None,
    track_button_noun = lambda feed : Person.from_feed(feed).him_her,
    thumbnail_image_url = feed_thumbnail_image_url,
    )
Feed.register_feed(
    "ps:",
    title = lambda feed : Person.from_feed(feed).name + " - Bills Sponsored",
    noun = "legislator",
    link = lambda feed: Person.from_feed(feed).get_absolute_url(),
    scoped_title = lambda feed : Person.from_feed(feed).lastname + "'s Sponsored Bills",
    category = "federal-bills",
    description = "You will get updates about major activity on bills sponsored by this Member of Congress.",
    thumbnail_image_url = feed_thumbnail_image_url,
    )
Feed.register_feed(
    "pv:",
    title = lambda feed : Person.from_feed(feed).name + " - Voting Record",
    noun = "legislator",
    link = lambda feed: Person.from_feed(feed).get_absolute_url(),
    scoped_title = lambda feed : Person.from_feed(feed).lastname + "'s Voting Record",
    single_event_type = True,
    category = "federal-votes",
    description = "You will get updates on how this Member of Congress votes in roll call votes.",
    thumbnail_image_url = feed_thumbnail_image_url,
)
