from flask import (
    Flask,
    render_template,
    request,
    redirect,
    session,
    jsonify,
    url_for,
    flash
)
import os
import subprocess

import random
import string
import logging
import requests
import secrets
import datetime
import itertools
import re

app = Flask(__name__)
app.secret_key = secrets.token_hex(16)


@app.template_filter('format_number')
def format_number(value):
    return "{:,}".format(value).replace(",", ".")

@app.template_filter('normalized_active')
def normalized_active(value):
    return os.path.isfile(os.path.join(get_hostfolder(), 'locations.d', value))


class Config:
    def __init__(self):
        self.FOLDER = '/var/cache/url-shortener'
        if os.path.isfile('/etc/url-shortener/conf.sh'):
            cfgmap = {
                'URL_SHORTENER_CACHE_BASE': 'FOLDER',
            }
            with open('/etc/url-shortener/conf.sh') as cf:
                while line := cf.readline().strip():
                    if not line or line.startswith('#'):
                        continue
                    mapname, value = line.split('=', 1)
                    if mapname in cfgmap:
                        setattr(self, cfgmap[mapname], value.strip('"\''))


logger = logging.getLogger(__name__)
config = Config()
FOLDER = config.FOLDER


def session_host():
    try:
        h = session['host']
    except:
        host, _err = exec_cmd(f"ls -1 {FOLDER}", return_first=True)
        session['host'] = host or 'default'
    return session['host']


def get_hostfolder():
    return f"{FOLDER}/{session_host()}"


def generate_random_hash(length=10):
    """Just generates a Hash"""
    characters = string.ascii_letters + string.digits
    hash_value = ''.join(random.choice(characters) for _ in range(length))
    return hash_value


def get_paginator(pages_total, page, page_limit=10):
    """Makes a paginator, that shows maximum 10 Items"""
    if not pages_total:
        return [], 0, 0
    paginator = []

    # Wenn die aktuelle Seite die erste oder letzte ist, zeige die ersten/letzten 10 Seiten an
    if page <= 5:
        paginator = list(range(1, min(pages_total + 1, 11)))
    elif page >= pages_total - 4:
        paginator = list(range(max(1, pages_total - 9), pages_total + 1))
    else:
        # Ansonsten zeige die aktuelle Seite an der 3. Stelle und die 7 folgenden Seiten
        paginator = [page - 2, page - 1, page, page + 1, page + 2, page + 3, page + 4, page + 5, page + 6, page + 7]
    return paginator, max(1, page - 1), min(pages_total, page + 1)


def exec_cmd(cmd, return_first=False):
    """Executes a shell command"""
    if request.args.get('verbose', 0, type=int):
        print(cmd)
    resp = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    msg = resp.stdout.decode().strip().split("\n")
    err = resp.stderr.decode().strip().split("\n")
    msg = list(filter(None, msg))
    err = list(filter(None, err))
    if return_first:
        return (msg[0], err) if msg else ('', err)
    return msg, err

def get_hosts():
    r = []
    for d in os.listdir(FOLDER):
        if os.path.isfile(f"{FOLDER}/{d}/.cachefile"):
            r.append(d)
    return r


def minimize_messages(msgs, max_length=20):
    if isinstance(msgs, str):
        return [msgs]
    if len(msgs) > max_length:
        cutter = (max_length // 2) - 1
        return msgs[:cutter] + ['[...]'] + msgs[-cutter:]
    return msgs

def ucfirst(self):
    s = str(self)
    return s[:1].upper() + s[1:]
def edit_response(message=[], url_for_kw={}, **kwargs):
    _msg = message or kwargs.get('message', [])
    msg=[]
    errors = []
    if isinstance(_msg, tuple):
        msg, errors = _msg
    if isinstance(_msg, str):
        msg = [_msg]

    kwargs['message'] = msg

    kwargs['error'] = set(list(kwargs.get('error', []) or []) + errors)

    if not request.is_json:
        message = kwargs.pop('message', [])
        error = kwargs.pop('error', [])
        if message:
            for m in minimize_messages(message):
                flash(ucfirst(m), 'success')
        if error:
            for e in minimize_messages(error):
                flash(ucfirst(e), 'danger')
        return redirect(url_for('edit', **url_for_kw))
    return jsonify(kwargs)

def get_request_data():
    if not request.is_json:
        return dict(
            action=request.form.get('action'),
            new_host=request.form.get('new_host'),
            hash=request.form.get('hash', '', type=str),
            status_code=request.form.get('status_code', 302),
            url=request.form.get('url', '', type=str).replace('$', ''),
            page=request.args.get('page', 1, type=int),
            term=request.args.get('term', '', type=str),
            error=request.args.get('error', ''),
            message=request.args.get('message', ''),
            restart=request.args.get('restart_nginx', 0, type=int),
        )
    else:
        try:
            if request.method not in ('GET', 'DELETE'):
                return request.get_json()
            else:
                return dict(
                    page=request.args.get('page', 1, type=int),
                    term=request.args.get('term', '', type=str),
                    hash=request.args.get('hash', '', type=str),
                )
        except Exception as e:
            logger.exception(e)
            return {
                'page': 1,
                'status_code': 302,
                'url': '',
                'term': '',
                'restart': False,
            }


def check_url(url):
    try:
        response = requests.get(url)
        if not response.ok:
            raise ValueError(f"{url} has no valid response")
    except Exception as e:
        logger.warning(str(e))
        raise ValueError(f"{url} has no valid response")


def get_actionname(request_data):
    if request.is_json:
        return {
            'PUT': 'edit',
            'PATCH': 'edit',
            'POST': 'create',
            'DELETE': 'delete',
            'GET': 'list',
        }[request.method]
    elif request.method == 'POST':
        return request_data.get('action')
    else:
        return 'list'


def restart_nginx():
    with open(f'{FOLDER}/.restart_nginx', 'w') as ng:
        ng.write(f"{datetime.datetime.now()}")
    return edit_response(message='Requested NGINX reload')

def create_host(hostname):
    return exec_cmd(f'/usr/bin/url-shortener create-host {hostname} 2>&1')



class Reloader:
    reload = False


reloader = Reloader()

normalize_re = re.compile('[^A-Za-z0-9]')

def normalize_filename(filename):
    return normalize_re.sub('-', filename)

class Editor:
    def __init__(self, hash_value=''):

        self.host = session_host()

        self.hostfolder = os.path.join(FOLDER, self.host)
        self.cachefile = f"{self.hostfolder}/.cachefile"

        self.conf_dir = self.hostfolder + '/nginx'
        self.locations_d_dir = self.hostfolder + '/locations.d'
        self.log_dir = self.hostfolder + '/logs'

        if hash_value:
            self.init_hash(hash_value)

    def init_hash(self, hash_value='', force_hash=False):
        if hash_value == 'edit':
            raise ValueError("Hash 'edit' is not allowed")

        if force_hash and not hash_value:
            raise ValueError("No hash given")

        hash_value = (hash_value.lstrip('/') or self.generate_new_hash())

        self.hash_value = hash_value.lstrip('/').replace('\\', '')
        self.normalized_hash = normalize_filename(self.hash_value)

        self.conf_file = f"{self.conf_dir}/{self.normalized_hash}"
        self.log_file = f"{self.log_dir}/{self.normalized_hash}"

    def link_location(self, unlink=False):
        if unlink:
            return exec_cmd(f"/usr/bin/url-shortener unlink-location {self.host} {self.normalized_hash}")
        return exec_cmd(f"/usr/bin/url-shortener link-location {self.host} {self.normalized_hash}")

    def remove_from_cache(self):
        _f = os.path.basename(self.cachefile)
        return exec_cmd(
            f"grep -v '{self.hash_value}' '{self.cachefile}' > /tmp/{_f}.tmp && mv /tmp/{_f}.tmp {self.cachefile}")

    def generate_new_hash(self, length=10):
        """Generates a Hash, that is not used"""
        hash_value = generate_random_hash(length)
        while os.path.isfile(self.conf_dir + '/' + hash_value) or os.path.isfile(self.log_dir + '/' + hash_value):
            hash_value = generate_random_hash(length)
        return hash_value

    def get_visits(self):
        """Get visits per Hash"""
        resp, _err = exec_cmd(f"wc -l {self.log_file} | sed 's/ .*$//'", return_first=True)
        return int(resp or 0)

    def get_files(self, page=1, per_page=100, term='', with_reload=False):
        # 0 elMnuj25s6 302 https://www.heise.de
        if term:
            gte0 = ''
            if term.endswith('>0'):
                gte0 = '/^[^0-9]*0[^0-9]/d;'
                term = term.replace('>0', '')
            lscommand = f"sed '{gte0}/{term.replace('/', r'\/')}/!d' {self.cachefile}"
        else:
            lscommand = f"cat {self.cachefile}"

        _cnt, _err = exec_cmd(lscommand + ' | wc -l', return_first=True)
        cnt = int(_cnt)

        if page > 1:
            tailstart = per_page * (page - 1) + 1
            tailer = f" | tail -n +{tailstart}"
        else:
            tailer = ""
        files_command = f"{lscommand} {tailer} | head -n {per_page}"
        _files, _err = exec_cmd(files_command)
        _files = map(lambda x: re.split(' +', x), _files)
        returns = list(_files)

        name_map = {}
        files  = []
        sizes = []
        sizes_map = {}
        nginx_activated = []

        if returns:
            nginx_activated, _err = exec_cmd(f"ls -1 {self.locations_d_dir}")
            name_map = {normalize_filename(f[1]):f[1] for f in returns}

            files = [f"{self.conf_dir}/{x}" for x in name_map]

            sizes, _err = exec_cmd(f"wc -l --total=never " + " ".join(
                map(lambda x: x.replace('/nginx/', '/logs/'), files)
            ) + " | sed '/^[^0-9]*0[^0-9]/d'")

        reload = False

        if sizes and sizes[0] != '0':
            for line in sizes:
                s, f = line.strip().split(' ')
                hash_value = name_map[os.path.basename(f)]
                sizes_map[hash_value] = (int(s), f)


        for _r in returns:
            _norm = normalize_filename(_r[1])
            if name_map.get(_r[1]) in sizes_map:
                _r[0] = sizes_map[name_map.get(_r[1])][0]
            _r.append(_norm)
            _r.append(_norm in nginx_activated)

        returns = sorted(returns, key=lambda x: int(x[0]), reverse=True)
        if request.args.get('update_cache', 0, type=int):
            _msg = []
            _err = []
            for hash_value, (s, f) in sizes_map.items():
                _msg_tmp, _err_tmp = exec_cmd(f"sed -i 's!^[^ ]* {hash_value} !{s} {hash_value} !' {self.cachefile}")
                if _err_tmp:
                    _err.extend(_err_tmp)
                else:
                    _msg.append(f"{s} from {f} updated in .cachefile")

            _tmpm, _tmpe = exec_cmd(f"/usr/bin/url-shortener sort-cache {self.host} 2>&1")
            _msg.extend(_tmpm)
            _err.extend(_tmpe)
            if _err:
                max_length = 10 if _msg else 20
                for _e in minimize_messages(_err, max_length):
                    flash(_e, 'danger')
            if _msg:
                max_length = 10 if _err else 20
                for _e in minimize_messages(_msg, max_length):
                    flash(_e, 'success')
            reload = True

        data = [cnt, list(files), returns]
        if with_reload:
            data.append(reload)
        return data

    def file_content(self, url, status=302):
        return (
            f"# {url}\n"
            f"location = /{self.hash_value} {{\n"
            f"    access_log {self.log_file};\n"
            f"    return {status} '{url}';\n"
            f"}}\n"
        )

    def edit_file(self, url, status_code, create=False, no_url_check=False):
        if not no_url_check:
            check_url(url)
        msg = "Added"
        if not create:
            cmd = f"sed -i 's!^[^ ]* {self.hash_value} .*!{self.get_visits()} {self.hash_value} {status_code} {url}!' {self.cachefile}"
            exec_cmd(cmd)
            msg = 'Changed'
        try:
            with open(self.conf_file, 'w') as file:
                file.write(self.file_content(url, status_code))
                return f"{msg} '{url}' as <a href=\"/{self.hash_value}\" target=\"_blank\">/{self.hash_value}</a>"
        except Exception as e:
            raise ValueError(str(e))

    def append_new_to_cache(self, status_code, url):
        try:
            with open(self.cachefile, 'a') as ch:
                ch.write(f"0 {self.hash_value} {str(status_code).strip()} {url.strip()}\n")
        except Exception as e:
            raise ValueError(str(e))

    def create_file(self, url, status_code, no_url_check=False):
        if os.path.isfile(self.conf_file):
            with open(self.conf_file, 'r') as fn:
                for line in fn:
                    url = line.strip().replace('#', '')
                    raise ValueError(f"/{self.hash_value} exists with URL {url}")
        try:
            if err := exec_cmd(f"touch {self.log_file}", return_first=True)[1]:
                raise ValueError(f"Could not touch {self.log_file}")
        except Exception as e:
            raise ValueError(str(e))

        self.append_new_to_cache(status_code, url)
        return self.edit_file(url, status_code, create=True, no_url_check=no_url_check)

    def delete_file(self):
        try:
            try:
                os.remove(self.log_file)
            except Exception as e:
                logger.exception(e)
            try:
                self.remove_from_cache()
            except Exception as e:
                logger.exception(e)
            os.remove(self.conf_file)
            return f"{self.hash_value} gelöscht"
        except Exception as e:
            logger.exception(e)
            raise ValueError(str(e))

    def purge(self):
        exec_cmd(f"find {self.conf_dir} -type f -delete")
        exec_cmd(f"find {self.log_dir} -type f -delete")
        restart_nginx()
        return "Purge successful"

    def generate(self, length=10):
        msg = []
        err = []
        domains = ['https://www.brainson.de', 'https://www.heise.de'] + \
                  [f'https://www.sixbid.com/{u}' for u in ['de', 'en', 'fr']]
        for i in range(0, length + 1):
            url = random.choice(domains)
            try:
                self.init_hash()
                msg.append(self.create_file(url, 302, no_url_check=True))
            except ValueError as e:
                err.append(str(e))
        return dict(message=msg, error=err)


@app.route('/edit', methods=['GET', 'POST', 'DELETE', 'PATCH', 'PUT'])
def edit():
    if _host := request.args.get('host', '', type=str):
        session['host'] = _host
        if not request.is_json:
            return edit_response()

    editor = Editor()

    per_page = request.args.get('per_page', 100, type=int)

    if request.args.get('purge', 0, type=int):
        return edit_response(editor.purge())

    if link_value := request.args.get('link_location', '', type=str):
        editor.init_hash(link_value, force_hash=True)
        return edit_response(message=editor.link_location())
    if link_value := request.args.get('unlink_location', '', type=str):
        editor.init_hash(link_value, force_hash=True)
        return edit_response(message=editor.link_location(unlink=True))

    if request.args.get('generate', 0, type=int):
        return edit_response(**editor.generate())

    request_data = get_request_data()

    if new_host := request_data.get('new_host', None):
        return edit_response(message=create_host(new_host), url_for_kw={'host': new_host})

    if request_data.get('restart_nginx'):
        return restart_nginx()

    page = request_data.get('page')
    term = request_data.get('term')
    hash_value = request_data.get('hash')
    status_code = request_data.get('status_code')
    url = request_data.get('url')
    error = request_data.get('error')
    message = request_data.get('message')

    action = get_actionname(request_data)

    try:
        editor.init_hash(hash_value, force_hash=True)
    except ValueError as e:
        if action in ('create', 'edit', 'delete'):
            return edit_response(error=str(e))

    try:
        if action in ('create', 'edit'):
            if url:
                if action == 'create':
                    return edit_response(message=editor.create_file(url, status_code))
                else:
                    return edit_response(message=editor.edit_file(url, status_code))
            else:
                raise ValueError("URL must be set")

        elif action == 'delete':
            return edit_response(message=editor.delete_file())

        if action != 'list':
            raise ValueError(f"Action '{action}' unknown")

    except ValueError as e:
        return edit_response(error=str(e))


    cnt, files, filedata, reload = editor.get_files(
        page=page,
        per_page=per_page,
        term=term,
        with_reload=True
    )

    if reload:
        return edit_response()

    total_pages = (int(cnt) + per_page - 1) // per_page
    start = (page - 1) * per_page

    datadict = {
        'cnt': cnt,
        'per_page': per_page,
        'newhash': editor.generate_new_hash(),
        'start': start,
        'end': start + per_page,
        'total_items': cnt,
        'files': files,
        'filedata': filedata,
        'message': message,
        'error': error,
        'current_page': page,
        'total_pages': total_pages,
        'term': term,
        'paginator': get_paginator(total_pages, page, per_page),
        'hosts': get_hosts(),
        'current_host': session_host(),
    }

    if request.is_json:
        return jsonify(datadict)

    return render_template('edit.html', pagesizes=[10, 25, 50, 100, 250, 500], **datadict)


if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('-P', '--port', default=9000, dest="port", required=False)
    parser.add_argument('-H', '--host', default='0.0.0.0', dest="host", required=False)
    parser.add_argument('-D', '--directory', default=FOLDER, dest="folder", required=False)
    args = parser.parse_args()

    FOLDER = args.folder
    app.run(debug=True, port=int(args.port), host=args.host)
