#!/usr/bin/env python3
"""Show the temporary call links SylkServer keeps (webrtcgateway call_links.py).

  sylk-call-links                  every account that has links, with their number
  sylk-call-links <account>        the links of one account
  sylk-call-links --token TOKEN    the account and link a token calls
"""
import argparse
import datetime
import json
import logging
import os
import sys
import time

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from application import log
from application.process import process

logging.getLogger('cassandra').setLevel(logging.WARNING)

FIELDS = ('token', 'name', 'created', 'expires', 'device_id', 'device_name', 'calls', 'last_call')


def _when(ms):
    try:
        return datetime.datetime.fromtimestamp(int(ms) / 1000).strftime('%Y-%m-%d %H:%M')
    except (TypeError, ValueError, OverflowError, OSError):
        return '-'


def _aor(account):
    account = account.strip().lower()
    return account[4:] if account.startswith('sip:') else account


class CassandraLinks(object):
    def __init__(self):
        from cassandra.cluster import EXEC_PROFILE_DEFAULT, Cluster, ExecutionProfile, NoHostAvailable
        from cassandra.cqlengine import connection
        from cassandra.policies import DCAwareRoundRobinPolicy
        from sylk.applications.webrtcgateway.configuration import CassandraConfig
        from sylk.applications.webrtcgateway.models.storage.cassandra import CallLink, CallLinkToken
        profile = ExecutionProfile(load_balancing_policy=DCAwareRoundRobinPolicy(), request_timeout=60)
        self.cluster = Cluster(CassandraConfig.cluster_contact_points, protocol_version=4, execution_profiles={EXEC_PROFILE_DEFAULT: profile})
        try:
            session = self.cluster.connect()
        except NoHostAvailable:
            log.warning("Can't connect to Cassandra cluster")
            sys.exit(1)
        connection.set_session(session)
        CallLink.__keyspace__ = CallLinkToken.__keyspace__ = CassandraConfig.keyspace
        self.link_model, self.token_model = CallLink, CallLinkToken
        print(f'Cassandra {", ".join(CassandraConfig.cluster_contact_points)}, keyspace {CassandraConfig.keyspace}, '
              f'tables {CallLink.__table_name__}, {CallLinkToken.__table_name__}\n')

    def _record(self, row):
        return dict({name: getattr(row, name) for name in FIELDS}, id=row.link_id, account=row.account)

    def links(self, account):
        return [self._record(row) for row in self.link_model.objects(account=account).limit(None)]

    def all_links(self):
        return [self._record(row) for row in self.link_model.objects.all().limit(None)]

    def by_token(self, token):
        row = self.token_model.objects(token=token).first()
        return (row.account, row.link_id) if row is not None else None

    def close(self):
        self.cluster.shutdown()


class FileLinks(object):
    def __init__(self):
        from sylk.applications.webrtcgateway.configuration import CallLinksConfig
        self.root = CallLinksConfig.storage_dir.normalized
        print(f'Files under {self.root}\n')

    @staticmethod
    def _read(path):
        try:
            with open(path) as f:
                return json.load(f)
        except (OSError, ValueError):
            return None

    def links(self, account):
        records = self._read(os.path.join(self.root, 'accounts', account[0], account + '.json')) or {}
        return [dict(record, account=account) for record in records.values()]

    def all_links(self):
        result = []
        base = os.path.join(self.root, 'accounts')
        for letter in sorted(os.listdir(base)) if os.path.isdir(base) else []:
            folder = os.path.join(base, letter)
            for name in sorted(os.listdir(folder)) if os.path.isdir(folder) else []:
                if name.endswith('.json'):
                    result.extend(self.links(name[:-len('.json')]))
        return result

    def by_token(self, token):
        entry = self._read(os.path.join(self.root, 'tokens', token[:2], token + '.json'))
        return (entry['account'], entry['id']) if isinstance(entry, dict) and entry.get('account') else None

    def close(self):
        pass


def state(link, now):
    return 'expired' if (link.get('expires') or 0) <= now else 'active'


def show_links(account, links):
    if not links:
        print(f'No call links for {account}')
        return
    now = int(time.time() * 1000)
    print(f'Call links of {account}:\n')
    for link in sorted(links, key=lambda link: link.get('created') or 0, reverse=True):
        print(f'  {link.get("name")}')
        print(f'      token     {link.get("token")}   ({state(link, now)})')
        print(f'      id        {link.get("id")}')
        print(f'      created   {_when(link.get("created"))}' + (f' by {link["device_name"]}' if link.get('device_name') else ''))
        print(f'      expires   {_when(link.get("expires"))}')
        print(f'      calls     {link.get("calls") or 0}' + (f', last {_when(link["last_call"])}' if link.get('last_call') else ''))
        print()
    active = sum(1 for link in links if state(link, now) == 'active')
    print(f'{len(links)} links, {active} active')


def show_accounts(links):
    if not links:
        print('No call links')
        return
    now = int(time.time() * 1000)
    accounts = {}
    for link in links:
        entry = accounts.setdefault(link['account'], [0, 0, 0])
        entry[0] += 1
        entry[1] += state(link, now) == 'active'
        entry[2] += link.get('calls') or 0
    width = max(len('account'), *(len(account) for account in accounts))
    print(f'{"account":<{width}}  {"links":>5}  {"active":>6}  {"calls":>5}')
    for account, (total, active, calls) in sorted(accounts.items(), key=lambda item: -item[1][0]):
        print(f'{account:<{width}}  {total:>5}  {active:>6}  {calls:>5}')


if __name__ == '__main__':
    process.configuration.subdirectory = 'sylkserver'

    parser = argparse.ArgumentParser(description='Show the temporary call links SylkServer keeps.')
    parser.add_argument('--config-dir', '--config-folder', dest='config_directory', default=None, metavar='PATH', help='Set a config directory.')
    parser.add_argument('account', nargs='?', help='only the links of this account')
    parser.add_argument('--token', help='the account and link this token calls')
    options = parser.parse_args()

    if options.config_directory is not None:
        process.configuration.local_directory = options.config_directory

    from sylk.server import ServerConfig
    ServerConfig.__cfgtype__(ServerConfig.__cfgfile__)

    from sylk.applications.webrtcgateway.configuration import CassandraConfig
    try:
        import cassandra.cqlengine  # noqa: F401
        use_cassandra = bool(CassandraConfig.cluster_contact_points)
    except ImportError:
        use_cassandra = False
    store = CassandraLinks() if use_cassandra else FileLinks()
    try:
        if options.token:
            token = options.token.strip().lower().split('@', 1)[0].rsplit('/', 1)[-1]
            found = store.by_token(token)
            if found is None:
                print(f'Token {token} calls nobody (unknown or expired past retention)')
            else:
                account, link_id = found
                show_links(account, [link for link in store.links(account) if link.get('id') == link_id])
        elif options.account:
            account = _aor(options.account)
            show_links(account, store.links(account))
        else:
            show_accounts(store.all_links())
    finally:
        store.close()
