#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import absolute_import


from io import open
from os import makedirs, sep, walk
from os.path import (
    abspath,
    dirname,
    exists,
    isdir,
    join,
    relpath,
)
from re import search, sub, IGNORECASE
from setuptools import setup
from setuptools.command.build_py import build_py as _build_py
from setuptools.command.egg_info import egg_info as _egg_info
from shutil import copy2, rmtree
from wheel.bdist_wheel import bdist_wheel as _bdist_wheel


REPO_BASE = 'https://gitlab.com/bontchev/siphoney/-/blob/master'

try:
    class bdist_wheel(_bdist_wheel):
        """
        Remove the egg-info directory after building the wheel.
        pip 19.x (Python 2.7) mistakes a leftover egg-info in the source tree
        for an existing installation and skips both copying files to
        site-packages and generating the entry point script.
        """
        def run(self):
            _bdist_wheel.run(self)
            here = dirname(abspath(__file__))
            egg_info = join(here, 'siphoney.egg-info')
            if isdir(egg_info):
                rmtree(egg_info)
    _have_wheel = True
except ImportError:
    _have_wheel = False


class egg_info(_egg_info):
    """
    Redirect the egg-info directory into build/ instead of the source root.

    When a user runs 'python setup.py bdist_wheel' the default behaviour is to
    write siphoney.egg-info into the source tree. pip2 (19.x) then finds this
    egg-info and mistakenly treats the source directory as an existing
    installation, causing 'pip2 install dist/siphoney-*.whl' to silently skip
    installing to site-packages and skip generating the console entry point.

    Redirecting to build/ avoids this: build/ is cleaned between builds and
    pip never looks there for installed packages.
    """
    def initialize_options(self):
        _egg_info.initialize_options(self)
        here = dirname(abspath(__file__))
        build_dir = join(here, 'build')
        if not isdir(build_dir):
            makedirs(build_dir)
        self.egg_base = build_dir


class build_py(_build_py):
    """
    Sync honeypot.py from the repo root into siphoney/ before building.
    This keeps a single source of truth (the root-level script) while ensuring
    the installed package contains it for use by the entry point.

    Note: 'python -m build' uses an isolated PEP 517 environment. The sdist
    already contains honeypot.py at the root, so when the wheel is built from
    the sdist the copy is present and this hook finds it correctly.
    """
    def run(self):
        here = dirname(abspath(__file__))
        src = join(here, 'honeypot.py')
        dst = join(here, 'siphoney', 'honeypot.py')
        if exists(src):
            copy2(src, dst)
        # Also copy Dockerfile from repo root into siphoney/data/ so that
        # `siphoney init` can place it in the working directory.
        dockerfile_src = join(here, 'Dockerfile')
        dockerfile_dst = join(here, 'siphoney', 'data', 'Dockerfile')
        if exists(dockerfile_src):
            copy2(dockerfile_src, dockerfile_dst)
        plugindoc_src = join(here, 'output_plugins', 'README.md')
        plugindoc_dst = join(here, 'siphoney', 'data', 'docs', 'PLUGINS.md')
        if exists(plugindoc_src):
            copy2(plugindoc_src, plugindoc_dst)
        _build_py.run(self)


def get_package_data():
    """
    Walk siphoney/ and return every non-.py file as a package_data entry.
    Files under data/ are always included even if they have a .py extension
    (e.g. data/test/test.py). Using os.walk means new files are picked up
    automatically without needing to update this list manually.
    """
    here = dirname(abspath(__file__))
    pkg_root = join(here, 'siphoney')
    result = []
    for dirpath, dirnames, filenames in walk(pkg_root):
        dirnames[:] = [d for d in dirnames
                       if d not in ('__pycache__', 'siphoney.egg-info')]
        for filename in filenames:
            abs_path = join(dirpath, filename)
            rel_path = relpath(abs_path, pkg_root)
            is_under_data = rel_path.startswith('data' + sep) or rel_path == 'data'
            # Regular .py package modules are handled by packages=; skip them.
            # Exception: honeypot.py lives directly in siphoney/ and is the
            # main script, included here so the entry point can import it.
            if filename.endswith('.py') and not is_under_data and filename != 'honeypot.py':
                continue
            # Dockerfile is copied into siphoney/data/ by the build hook;
            # include it explicitly even if it isn't present in the source tree.
            result.append(rel_path.replace(sep, '/'))
    # Always include these hook-generated files, whether present yet or not.
    for extra in ('honeypot.py', join('data', 'Dockerfile')):
        entry = extra.replace(sep, '/')
        if entry not in result:
            result.append(entry)
    return result


def get_version():
    here = dirname(abspath(__file__))
    with open(join(here, 'honeypot.py')) as fh:
        content = fh.read()
    match = search(r"__VERSION__\s*=\s*['\"]([^'\"]+)['\"]", content, IGNORECASE)
    if not match:
        raise RuntimeError('Cannot find __VERSION__ in honeypot.py')
    return match.group(1)


# Copy hook-generated files into the package directory before setup() runs,
# so they are present when setuptools assembles the wheel's file list.
# This is necessary because get_package_data() is called at setup() time,
# before any build hooks run, so the files must already exist on disk.
_here = dirname(abspath(__file__))
for _src, _dst in [
    (join(_here, 'honeypot.py'), join(_here, 'siphoney', 'honeypot.py')),
    (join(_here, 'Dockerfile'),  join(_here, 'siphoney', 'data', 'Dockerfile')),
    (join(_here, 'output_plugins', 'README.md'), join(_here, 'siphoney', 'data', 'docs', 'PLUGINS.md')),
]:
    if exists(_src):
        copy2(_src, _dst)


def make_pypi_readme():
    with open('README.md', encoding='utf-8') as fh:
        content = fh.read()
    # Rewrite markdown links: [text](relative/path) -> [text](REPO_BASE/relative/path)
    def absolutify(m):
        url = m.group(2)
        if url.startswith('http://') or url.startswith('https://') or url.startswith('#'):
            return m.group(0)  # already absolute or anchor, leave alone
        return '[{}]({}/{})'.format(m.group(1), REPO_BASE, url)
    return sub(r'\[([^\]]+)\]\(([^)]+)\)', absolutify, content)


extras={
    'couchdb': ['couchdb'],
    'datadog': [
        'cryptography<=2.8 ; python_version < "3"',
        'pyOpenSSL<=18.0.0 ; python_version < "3"',
        'cryptography ; python_version >= "3"',
        'pyOpenSSL ; python_version >= "3"',
    ],
    'discord': [],
    'elastic': [
        'elasticsearch<=7.13 ; python_version < "3"',
        'numpy<=1.16.6 ; python_version < "3"',
        'elasticsearch8>=8.12.0,<9.0.0 ; python_version >= "3"',
        'numpy ; python_version >= "3"',
    ],
    'hpfeed': [
        'Automat<20 ; python_version < "3"',
        'hpfeeds>=3.0.0',
    ],
    'influx2': ['influxdb-client ; python_version >= "3"'],  # Python 3 only
    'jsonlog': [],
    'kafka': [
        'confluent-kafka<1.0 ; python_version < "3"',
        'confluent-kafka ; python_version >= "3"',
    ],
    'localsyslog': [],
    'mongodb': [
        'pymongo<=3.13.0 ; python_version < "3"',
        'dnspython ; python_version < "3"',
        'pymongo ; python_version >= "3"',
    ],
    'mysql': [
        'PyMySQL ; python_version < "3"',
        'mysqlclient>=1.3.12 ; python_version >= "3"',
    ],
    'nlcvapi': [
        'pyOpenSSL<=18.0.0 ; python_version < "3"',
        'pyOpenSSL ; python_version >= "3"',
    ],
    'postgres': ['psycopg2-binary'],
    'redisdb': [
        'redis<=3.5.3 ; python_version < "3"',
        'redis ; python_version >= "3"',
    ],
    'rethinkdblog': [
        'rethinkdb>=2.4',
        'looseversion',
    ],
    'slack': [
        'slackclient<3 ; python_version < "3"',
        'slack-sdk ; python_version >= "3"',
    ],
    'socketlog': [],
    'sqlite': [],
    'telegram': [],
    'textlog': [],
    'xmpp': ['xmpppy>=0.7.3'],
}

extras['all'] = sorted({
    dep
    for name, deps in extras.items()
    if name != 'all'
    for dep in deps
})

setup(
    name='siphoney',
    version=get_version(),
    description='A SIP Honeypot',
    long_description=make_pypi_readme(),
    long_description_content_type='text/markdown',
    author='Vesselin Bontchev',
    author_email='vbontchev@yahoo.com',
    url='https://gitlab.com/bontchev/siphoney',
    license='GPL-3.0-only',
    classifiers=[
        'Development Status :: 5 - Production/Stable',
        'Environment :: Console',
        'Intended Audience :: Information Technology',
        'Intended Audience :: Science/Research',
        'Intended Audience :: System Administrators',
        'Operating System :: Microsoft :: Windows',
        'Operating System :: POSIX :: Linux',
        'Programming Language :: Python :: 2',
        'Programming Language :: Python :: 2.7',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.6',
        'Programming Language :: Python :: 3.7',
        'Programming Language :: Python :: 3.8',
        'Programming Language :: Python :: 3.9',
        'Programming Language :: Python :: 3.10',
        'Programming Language :: Python :: 3.11',
        'Programming Language :: Python :: 3.12',
        'Programming Language :: Python :: 3.13',
        'Programming Language :: Python :: 3.14',
        'Topic :: Security',
    ],
    python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*',

    # -------------------------------------------------------------------------
    # Installed packages:
    #   - siphoney       the console entry point + all bundled data assets
    #   - core           the honeypot engine (config, protocol, tools, ...)
    #   - output_plugins one module per output backend
    # -------------------------------------------------------------------------
    packages=['siphoney', 'core', 'output_plugins'],
    package_data={
        'siphoney': get_package_data(),
    },

    cmdclass=dict(
        {'build_py': build_py},
        **({'bdist_wheel': bdist_wheel} if _have_wheel else {})
    ),

    entry_points={
        'console_scripts': [
            'siphoney = siphoney.cli:main',
        ],
    },

    install_requires=[
        'configparser>=3.5.0',
        'geoip2>=2.7.0',
        'ipaddress ; python_version < "3"',
        'maxminddb>=1.3.0',
        'pytz',
        'requests<=2.27.1 ; python_version < "3"',
        'requests ; python_version >= "3"',
        'service_identity<=18.1.0 ; python_version < "3"',
        'service_identity ; python_version >= "3"',
        'twisted>=20.3.0,<21 ; python_version < "3"',
        'twisted>=21 ; python_version >= "3"',
    ],

    extras_require = extras
)
