# -*- coding: utf-8 -*-

"""
netease-dl.weapi
~~~~~~~~~~~~~~~~

This module provides a Crawler class to get NetEase Music API.
"""
import re
import hashlib
import os
import sys

import click
import requests
from requests.exceptions import RequestException, Timeout, ProxyError
from requests.exceptions import ConnectionError as ConnectionException

from .compat import cookielib
from .encrypt import encrypted_request
from .utils import Display
from .config import headers, cookie_path, person_info_path
from .logger import get_logger
from .exceptions import (
    SearchNotFound, SongNotAvailable, GetRequestIllegal, PostRequestIllegal)
from .models import Song, Album, Artist, Playlist, User


LOG = get_logger(__name__)


def exception_handle(method):
    """Handle exception raised by requests library."""

    def wrapper(*args, **kwargs):
        try:
            result = method(*args, **kwargs)
            return result
        except ProxyError:
            LOG.exception('ProxyError when try to get %s.', args)
            raise ProxyError('A proxy error occurred.')
        except ConnectionException:
            LOG.exception('ConnectionError when try to get %s.', args)
            raise ConnectionException('DNS failure, refused connection, etc.')
        except Timeout:
            LOG.exception('Timeout when try to get %s', args)
            raise Timeout('The request timed out.')
        except RequestException:
            LOG.exception('RequestException when try to get %s.', args)
            raise RequestException('Please check out your network.')

    return wrapper


class Crawler(object):
    """NetEase Music API."""

    def __init__(self, timeout=60, proxy=None):
        self.session = requests.Session()
        self.session.headers.update(headers)
        self.session.cookies = cookielib.LWPCookieJar(cookie_path)
        self.download_session = requests.Session()
        self.timeout = timeout
        self.proxies = {'http': proxy, 'https': proxy}

        self.display = Display()

    @exception_handle
    def get_request(self, url):
        """Send a get request.

        warning: old api.
        :return: a dict or raise Exception.
        """

        resp = self.session.get(url, timeout=self.timeout,
                                proxies=self.proxies)
        result = resp.json()
        if result['code'] != 200:
            LOG.error('Return %s when try to get %s', result, url)
            raise GetRequestIllegal(result)
        else:
            return result

    @exception_handle
    def post_request(self, url, params):
        """Send a post request.

        :return: a dict or raise Exception.
        """

        data = encrypted_request(params)
        resp = self.session.post(url, data=data, timeout=self.timeout,
                                 proxies=self.proxies)
        result = resp.json()
        if result['code'] != 200:
            LOG.error('Return %s when try to post %s => %s',
                      result, url, params)
            raise PostRequestIllegal(result)
        else:
            return result

    def search(self, search_content, search_type, limit=9):
        """Search entrance.

        :params search_content: search content.
        :params search_type: search type.
        :params limit: result count returned by weapi.
        :return: a dict.
        """

        url = 'http://music.163.com/weapi/cloudsearch/get/web?csrf_token='
        params = {'s': search_content, 'type': search_type, 'offset': 0,
                  'sub': 'false', 'limit': limit}
        result = self.post_request(url, params)
        return result

    def search_song(self, song_name, quiet=False, limit=9):
        """Search song by song name.

        :params song_name: song name.
        :params quiet: automatically select the best one.
        :params limit: song count returned by weapi.
        :return: a Song object.
        """

        result = self.search(song_name, search_type=1, limit=limit)

        if result['result']['songCount'] <= 0:
            LOG.warning('Song %s not existed!', song_name)
            raise SearchNotFound('Song {} not existed.'.format(song_name))
        else:
            songs = result['result']['songs']
            if quiet:
                song_id, song_name = songs[0]['id'], songs[0]['name']
                song = Song(song_id, song_name)
                return song
            else:
                return self.display.select_one_song(songs)

    def search_album(self, album_name, quiet=False, limit=9):
        """Search album by album name.

        :params album_name: album name.
        :params quiet: automatically select the best one.
        :params limit: album count returned by weapi.
        :return: a Album object.
        """

        result = self.search(album_name, search_type=10, limit=limit)

        if result['result']['albumCount'] <= 0:
            LOG.warning('Album %s not existed!', album_name)
            raise SearchNotFound('Album {} not existed'.format(album_name))
        else:
            albums = result['result']['albums']
            if quiet:
                album_id, album_name = albums[0]['id'], albums[0]['name']
                album = Album(album_id, album_name)
                return album
            else:
                return self.display.select_one_album(albums)

    def search_artist(self, artist_name, quiet=False, limit=9):
        """Search artist by artist name.

        :params artist_name: artist name.
        :params quiet: automatically select the best one.
        :params limit: artist count returned by weapi.
        :return: a Artist object.
        """

        result = self.search(artist_name, search_type=100, limit=limit)

        if result['result']['artistCount'] <= 0:
            LOG.warning('Artist %s not existed!', artist_name)
            raise SearchNotFound('Artist {} not existed.'.format(artist_name))
        else:
            artists = result['result']['artists']
            if quiet:
                artist_id, artist_name = artists[0]['id'], artists[0]['name']
                artist = Artist(artist_id, artist_name)
                return artist
            else:
                return self.display.select_one_artist(artists)

    def search_playlist(self, playlist_name, quiet=False, limit=9):
        """Search playlist by playlist name.

        :params playlist_name: playlist name.
        :params quiet: automatically select the best one.
        :params limit: playlist count returned by weapi.
        :return: a Playlist object.
        """

        result = self.search(playlist_name, search_type=1000, limit=limit)

        if result['result']['playlistCount'] <= 0:
            LOG.warning('Playlist %s not existed!', playlist_name)
            raise SearchNotFound('playlist {} not existed'.format(playlist_name))
        else:
            playlists = result['result']['playlists']
            if quiet:
                playlist_id, playlist_name = playlists[0]['id'], playlists[0]['name']
                playlist = Playlist(playlist_id, playlist_name)
                return playlist
            else:
                return self.display.select_one_playlist(playlists)

    def search_user(self, user_name, quiet=False, limit=9):
        """Search user by user name.

        :params user_name: user name.
        :params quiet: automatically select the best one.
        :params limit: user count returned by weapi.
        :return: a User object.
        """

        result = self.search(user_name, search_type=1002, limit=limit)

        if result['result']['userprofileCount'] <= 0:
            LOG.warning('User %s not existed!', user_name)
            raise SearchNotFound('user {} not existed'.format(user_name))
        else:
            users = result['result']['userprofiles']
            if quiet:
                user_id, user_name = users[0]['userId'], users[0]['nickname']
                user = User(user_id, user_name)
                return user
            else:
                return self.display.select_one_user(users)

    def get_user_playlists(self, user_id, limit=1000):
        """Get a user's all playlists.

        warning: login is required for private playlist.
        :params user_id: user id.
        :params limit: playlist count returned by weapi.
        :return: a Playlist Object.
        """

        url = 'http://music.163.com/weapi/user/playlist?csrf_token='
        csrf = ''
        params = {'offset': 0, 'uid': user_id, 'limit': limit,
                  'csrf_token': csrf}
        result = self.post_request(url, params)
        playlists = result['playlist']
        return self.display.select_one_playlist(playlists)

    def get_playlist_songs(self, playlist_id, limit=1000):
        """Get a playlists's all songs.

        :params playlist_id: playlist id.
        :params limit: length of result returned by weapi.
        :return: a list of Song object.
        """

        url = 'http://music.163.com/weapi/v3/playlist/detail?csrf_token='
        csrf = ''
        params = {'id': playlist_id, 'offset': 0, 'total': True,
                  'limit': limit, 'n': 1000, 'csrf_token': csrf}
        result = self.post_request(url, params)

        songs = result['playlist']['tracks']
        songs = [Song(song['id'], song['name']) for song in songs]
        return songs

    def get_album_songs(self, album_id):
        """Get a album's all songs.

        warning: use old api.
        :params album_id: album id.
        :return: a list of Song object.
        """

        url = 'http://music.163.com/api/album/{}/'.format(album_id)
        result = self.get_request(url)

        songs = result['album']['songs']
        songs = [Song(song['id'], song['name']) for song in songs]
        return songs

    def get_artists_hot_songs(self, artist_id):
        """Get a artist's top50 songs.

        warning: use old api.
        :params artist_id: artist id.
        :return: a list of Song object.
        """
        url = 'http://music.163.com/api/artist/{}'.format(artist_id)
        result = self.get_request(url)

        hot_songs = result['hotSongs']
        songs = [Song(song['id'], song['name']) for song in hot_songs]
        return songs

    def get_song_url(self, song_id, bit_rate=320000):
        """Get a song's download address.

        :params song_id: song id<int>.
        :params bit_rate: {'MD 128k': 128000, 'HD 320k': 320000}
        :return: a song's download address.
        """

        url = 'http://music.163.com/weapi/song/enhance/player/url?csrf_token='
        csrf = ''
        params = {'ids': [song_id], 'br': bit_rate, 'csrf_token': csrf}
        result = self.post_request(url, params)
        song_url = result['data'][0]['url']  # download address

        if song_url is None:  # Taylor Swift's song is not available
            LOG.warning(
                'Song %s is not available due to copyright issue. => %s',
                song_id, result)
            raise SongNotAvailable(
                'Song {} is not available due to copyright issue.'.format(song_id))
        else:
            return song_url

    def get_song_lyric(self, song_id):
        """Get a song's lyric.

        warning: use old api.
        :params song_id: song id.
        :return: a song's lyric.
        """

        url = 'http://music.163.com/api/song/lyric?os=osx&id={}&lv=-1&kv=-1&tv=-1'.format(  # NOQA
            song_id)
        result = self.get_request(url)
        if 'lrc' in result and result['lrc']['lyric'] is not None:
            lyric_info = result['lrc']['lyric']
        else:
            lyric_info = 'Lyric not found.'
        return lyric_info

    @exception_handle
    def get_song_by_url(self, song_url, song_name, folder, lyric_info):
        """Download a song and save it to disk.

        :params song_url: download address.
        :params song_name: song name.
        :params folder: storage path.
        :params lyric: lyric info.
        """

        if not os.path.exists(folder):
            os.makedirs(folder)
        fpath = os.path.join(folder, song_name+'.mp3')

        if sys.platform == 'win32' or sys.platform == 'cygwin':
            valid_name = re.sub(r'[<>:"/\\|?*]', '', song_name)
            if valid_name != song_name:
                click.echo('{} will be saved as: {}.mp3'.format(song_name, valid_name))
                fpath = os.path.join(folder, valid_name + '.mp3')

        if not os.path.exists(fpath):
            resp = self.download_session.get(
                song_url, timeout=self.timeout, stream=True)
            length = int(resp.headers.get('content-length'))
            label = 'Downloading {} {}kb'.format(song_name, int(length/1024))

            with click.progressbar(length=length, label=label) as progressbar:
                with open(fpath, 'wb') as song_file:
                    for chunk in resp.iter_content(chunk_size=1024):
                        if chunk:  # filter out keep-alive new chunks
                            song_file.write(chunk)
                            progressbar.update(1024)

        if lyric_info:
            folder = os.path.join(folder, 'lyric')
            if not os.path.exists(folder):
                os.makedirs(folder)
            fpath = os.path.join(folder, song_name+'.lrc')
            with open(fpath, 'w') as lyric_file:
                lyric_file.write(lyric_info)

    def login(self):
        """Login entrance."""
        username = click.prompt('Please enter your email or phone number')
        password = click.prompt('Please enter your password', hide_input=True)

        pattern = re.compile(r'^0\d{2,3}\d{7,8}$|^1[34578]\d{9}$')
        if pattern.match(username):  # use phone number to login
            url = 'https://music.163.com/weapi/login/cellphone'
            params = {
                'phone': username,
                'password': hashlib.md5(password.encode('utf-8')).hexdigest(),
                'rememberLogin': 'true'}
        else:  # use email to login
            url = 'https://music.163.com/weapi/login?csrf_token='
            params = {
                'username': username,
                'password': hashlib.md5(password.encode('utf-8')).hexdigest(),
                'rememberLogin': 'true'}

        try:
            result = self.post_request(url, params)
        except PostRequestIllegal:
            click.echo('Password Error!')
            sys.exit(1)
        self.session.cookies.save()
        uid = result['account']['id']
        with open(person_info_path, 'w') as person_info:
            person_info.write(str(uid))