#!/usr/bin/env python3
# web-publish v1.9 — Publish 1C infobase via Apache (+_version_dir/_version_key: общий эталон db-семейства)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills

"""
Публикация информационной базы 1С через Apache HTTP Server.
Генерирует default.vrd и настраивает httpd.conf для веб-доступа
к информационной базе 1С. При необходимости скачивает portable Apache.
Идемпотентный — повторный вызов обновляет конфигурацию.
"""

import argparse
import glob
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
import urllib.request
import zipfile

import psutil

# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
    """parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
    argv = list(sys.argv[1:] if argv is None else argv)
    names = {s.lower(): s for a in parser._actions for s in a.option_strings}
    for i, tok in enumerate(argv):
        if tok.startswith('-') and tok.lower() in names:
            argv[i] = names[tok.lower()]
    # choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
    choice_map = {}
    for a in parser._actions:
        if a.choices:
            for s in a.option_strings:
                choice_map[s] = {str(c).lower(): c for c in a.choices}
    for i in range(len(argv) - 1):
        m = choice_map.get(argv[i])
        if m and argv[i + 1].lower() in m:
            argv[i + 1] = m[argv[i + 1].lower()]
    return parser.parse_args(argv)



def _find_project_v8path():
    """Walk up from CWD to find .v8-project.json and read its v8path."""
    d = os.getcwd()
    while True:
        pf = os.path.join(d, ".v8-project.json")
        if os.path.isfile(pf):
            try:
                with open(pf, encoding="utf-8-sig") as f:
                    data = json.load(f)
                v = data.get("v8path")
                if v:
                    return v
            except Exception:
                pass
            return None
        parent = os.path.dirname(d)
        if parent == d:
            return None
        d = parent


def _version_dir(p):
    """Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
    parent = os.path.dirname(p)
    if os.path.basename(parent).lower() == "bin":
        parent = os.path.dirname(parent)
    return os.path.basename(parent)


def _version_key(p):
    """Numeric sort key from version dir name."""
    return [int(x) for x in re.findall(r"\d+", _version_dir(p))]


def get_our_httpd(httpd_exe_norm):
    """Filter httpd processes by our ApachePath."""
    result = []
    if not httpd_exe_norm:
        return result
    for p in psutil.process_iter(['pid', 'name', 'exe']):
        try:
            if p.info['name'] and 'httpd' in p.info['name'].lower():
                if p.info['exe'] and os.path.normcase(os.path.normpath(p.info['exe'])) == httpd_exe_norm:
                    result.append(p)
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            pass
    return result


def get_all_httpd():
    """Get all httpd processes."""
    result = []
    for p in psutil.process_iter(['pid', 'name', 'exe']):
        try:
            if p.info['name'] and 'httpd' in p.info['name'].lower():
                result.append(p)
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            pass
    return result


def check_port_in_use(port):
    """Check if a port is in use and return the owning PID, or None."""
    for conn in psutil.net_connections(kind='tcp'):
        if conn.laddr and conn.laddr.port == port and conn.status == 'LISTEN':
            return conn.pid
    return None


def main():
    sys.stdout.reconfigure(encoding="utf-8")
    sys.stderr.reconfigure(encoding="utf-8")
    parser = argparse.ArgumentParser(description='Publish 1C infobase via Apache', allow_abbrev=False)
    parser.add_argument('-V8Path', type=str, default='', help='Path to 1C platform bin directory (for wsap24.dll)')
    parser.add_argument('-InfoBasePath', type=str, default='', help='Path to file infobase')
    parser.add_argument('-InfoBaseServer', type=str, default='', help='1C server (for server infobase)')
    parser.add_argument('-InfoBaseRef', type=str, default='', help='Infobase name on server')
    parser.add_argument('-UserName', type=str, default='', help='1C user name')
    parser.add_argument('-Password', type=str, default='', help='1C password')
    parser.add_argument('-AppName', type=str, default='', help='Publication name (default: from infobase folder name)')
    parser.add_argument('-ApachePath', type=str, default='', help='Apache root (default: tools\\apache24)')
    parser.add_argument('-Port', type=int, default=8081, help='Port (default: 8081)')
    parser.add_argument('-Manual', action='store_true', help='Do not download Apache — only check and give instructions')
    args = ci_parse_args(parser)

    # --- Resolve V8Path ---
    v8_path = args.V8Path
    if not v8_path:
        v8_path = _find_project_v8path()
    if not v8_path:
        candidates = (
            glob.glob(r'C:\Program Files\1cv8\*\bin\1cv8.exe')
            + glob.glob(r'C:\Program Files (x86)\1cv8\*\bin\1cv8.exe')
        )
        if candidates:
            best = max(candidates, key=_version_key)
            v8_path = os.path.dirname(best)
            ver = os.path.basename(os.path.dirname(v8_path))
            print(f'Auto-selected platform {ver}: {v8_path}')
        else:
            print('Error: платформа 1С не найдена. Укажите -V8Path')
            sys.exit(1)
    elif os.path.isfile(v8_path):
        v8_path = os.path.dirname(v8_path)

    # Validate wsap24.dll
    wsap_dll = os.path.join(v8_path, 'wsap24.dll')
    if not os.path.exists(wsap_dll):
        print(f'Error: wsap24.dll не найден в {v8_path}')
        sys.exit(1)

    # --- Validate connection ---
    if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
        print('Error: укажите -InfoBasePath или -InfoBaseServer + -InfoBaseRef')
        sys.exit(1)

    # --- Resolve ApachePath ---
    apache_path = args.ApachePath
    if not apache_path:
        script_dir = os.path.dirname(os.path.abspath(__file__))
        project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(script_dir))))
        apache_path = os.path.join(project_root, 'tools', 'apache24')
    # Ensure absolute path (agent may pass relative like "tools/apache24")
    if not os.path.isabs(apache_path):
        apache_path = os.path.abspath(apache_path)

    port = args.Port

    # --- Check / Install Apache ---
    httpd_exe = os.path.join(apache_path, 'bin', 'httpd.exe')

    if not os.path.exists(httpd_exe):
        if args.Manual:
            print(f'Apache не найден: {apache_path}')
            print('')
            print('Установите Apache вручную:')
            print('  1. Скачайте Apache Lounge (x64) с https://www.apachelounge.com/download/')
            print(f'  2. Распакуйте содержимое Apache24\\ в: {apache_path}')
            print('  3. Запустите скрипт повторно')
            sys.exit(1)

        print('Apache не найден. Скачиваю...')
        tmp_zip = os.path.join(tempfile.gettempdir(), 'apache24.zip')
        tmp_dir = os.path.join(tempfile.gettempdir(), 'apache24_extract')

        try:
            # Parse Apache Lounge download page for latest Win64 zip URL
            download_page = 'https://www.apachelounge.com/download/'
            print(f'Определяю актуальную версию с {download_page} ...')
            req = urllib.request.Request(download_page, headers={
                'User-Agent': 'Mozilla/5.0',
            })
            with urllib.request.urlopen(req, timeout=30) as resp:
                html = resp.read().decode('utf-8', errors='replace')

            # Links are typically relative (/download/...), try that first
            m = re.search(r'(?i)href="(/download/[^"]*?httpd-[^"]*?Win64[^"]*?\.zip)"', html)
            if not m:
                m = re.search(r'(?i)href="(https://[^"]*?httpd-[^"]*?Win64[^"]*?\.zip)"', html)

            if m:
                zip_url = m.group(1)
                if zip_url.startswith('/'):
                    zip_url = f'https://www.apachelounge.com{zip_url}'
                print(f'Найдено: {zip_url}')
            else:
                print('Не удалось определить ссылку автоматически.')
                print(f'Скачайте вручную: {download_page}')
                sys.exit(1)

            urllib.request.urlretrieve(zip_url, tmp_zip)
        except SystemExit:
            raise
        except Exception as e:
            print(f'Error: не удалось скачать Apache: {e}')
            print('Скачайте вручную: https://www.apachelounge.com/download/')
            sys.exit(1)

        print('Распаковка...')
        if os.path.exists(tmp_dir):
            shutil.rmtree(tmp_dir, ignore_errors=True)

        with zipfile.ZipFile(tmp_zip, 'r') as zf:
            zf.extractall(tmp_dir)

        # Move Apache24 contents up to ApachePath
        inner_dir = os.path.join(tmp_dir, 'Apache24')
        if not os.path.isdir(inner_dir):
            # Try to find Apache24 in nested folder
            found_inner = None
            for root, dirs, files in os.walk(tmp_dir):
                if 'Apache24' in dirs:
                    found_inner = os.path.join(root, 'Apache24')
                    break
            if found_inner:
                inner_dir = found_inner
            else:
                print('Error: каталог Apache24 не найден в архиве')
                sys.exit(1)

        os.makedirs(apache_path, exist_ok=True)
        # Copy contents of inner_dir to apache_path
        for item in os.listdir(inner_dir):
            src = os.path.join(inner_dir, item)
            dst = os.path.join(apache_path, item)
            if os.path.isdir(src):
                if os.path.exists(dst):
                    shutil.rmtree(dst)
                shutil.copytree(src, dst)
            else:
                shutil.copy2(src, dst)

        # Cleanup
        try:
            os.remove(tmp_zip)
        except OSError:
            pass
        try:
            shutil.rmtree(tmp_dir, ignore_errors=True)
        except OSError:
            pass

        # Patch ServerRoot in httpd.conf
        conf_file = os.path.join(apache_path, 'conf', 'httpd.conf')
        if os.path.exists(conf_file):
            apache_path_fwd = apache_path.replace('\\', '/')
            with open(conf_file, 'r', encoding='utf-8-sig') as f:
                conf_content = f.read()
            conf_content = re.sub(
                r'(?m)^Define SRVROOT .*$',
                f'Define SRVROOT "{apache_path_fwd}"',
                conf_content,
            )
            with open(conf_file, 'w', encoding='utf-8') as f:
                f.write(conf_content)
            print(f'ServerRoot обновлён: {apache_path_fwd}')

        print(f'Apache установлен: {apache_path}')

    # --- Derive AppName ---
    app_name = args.AppName
    if not app_name:
        if args.InfoBasePath:
            app_name = re.sub(r'[^\w]', '', os.path.basename(args.InfoBasePath))
        else:
            app_name = re.sub(r'[^\w]', '', args.InfoBaseRef)
        app_name = app_name.lower()
    app_name = app_name.lower()

    if not app_name:
        print('Error: не удалось определить имя публикации. Укажите -AppName')
        sys.exit(1)

    print(f'Публикация: {app_name}')

    # --- Create publish directory ---
    publish_dir = os.path.join(apache_path, 'publish', app_name)
    os.makedirs(publish_dir, exist_ok=True)

    # --- Generate default.vrd ---
    vrd_path = os.path.join(publish_dir, 'default.vrd')

    ib_parts = []
    if args.InfoBaseServer and args.InfoBaseRef:
        ib_parts.append(f'Srvr=&quot;{args.InfoBaseServer}&quot;')
        ib_parts.append(f'Ref=&quot;{args.InfoBaseRef}&quot;')
    else:
        ib_parts.append(f'File=&quot;{args.InfoBasePath}&quot;')
    if args.UserName:
        ib_parts.append(f'Usr=&quot;{args.UserName}&quot;')
    if args.Password:
        ib_parts.append(f'Pwd=&quot;{args.Password}&quot;')
    ib_string = ';'.join(ib_parts) + ';'

    vrd_content = f'''<?xml version="1.0" encoding="UTF-8"?>
<point xmlns="http://v8.1c.ru/8.2/virtual-resource-system"
       xmlns:xs="http://www.w3.org/2001/XMLSchema"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       base="/{app_name}"
       ib="{ib_string}">
    <standardOdata enable="true"/>
    <ws pointEnableCommon="true" publishExtensionsByDefault="true"/>
    <httpServices publishByDefault="true" publishExtensionsByDefault="true"/>
</point>'''

    with open(vrd_path, 'wb') as f:
        f.write(b'\xef\xbb\xbf')
        f.write(vrd_content.encode('utf-8'))
    print(f'default.vrd: {vrd_path}')

    # --- Update httpd.conf ---
    conf_file = os.path.join(apache_path, 'conf', 'httpd.conf')
    if not os.path.exists(conf_file):
        print(f'Error: httpd.conf не найден: {conf_file}')
        sys.exit(1)

    with open(conf_file, 'r', encoding='utf-8-sig') as f:
        conf_content = f.read()

    apache_path_fwd = apache_path.replace('\\', '/')
    wsap_dll_fwd = wsap_dll.replace('\\', '/')
    publish_dir_fwd = publish_dir.replace('\\', '/')
    vrd_path_fwd = vrd_path.replace('\\', '/')

    # --- Global block (Listen + LoadModule) ---
    global_marker_start = '# --- 1C: global ---'
    global_marker_end = '# --- End: global ---'
    global_block = (
        f'{global_marker_start}\n'
        f'Listen {port}\n'
        f'LoadModule _1cws_module "{wsap_dll_fwd}"\n'
        f'{global_marker_end}'
    )

    if re.search(re.escape(global_marker_start), conf_content):
        # Replace existing global block
        pattern = re.escape(global_marker_start) + r'[\s\S]*?' + re.escape(global_marker_end)
        conf_content = re.sub(pattern, global_block, conf_content)
    else:
        # Comment out default Listen to avoid port conflict
        conf_content = re.sub(r'(?m)^(Listen\s+\d+)', r'#\1  # commented by web-publish', conf_content)
        # Append global block
        conf_content = conf_content.rstrip() + '\n\n' + global_block + '\n'

    # --- Publication block ---
    pub_marker_start = f'# --- 1C Publication: {app_name} ---'
    pub_marker_end = f'# --- End: {app_name} ---'
    pub_block = (
        f'{pub_marker_start}\n'
        f'Alias "/{app_name}" "{publish_dir_fwd}"\n'
        f'<Directory "{publish_dir_fwd}">\n'
        f'    AllowOverride All\n'
        f'    Require all granted\n'
        f'    SetHandler 1c-application\n'
        f'    ManagedApplicationDescriptor "{vrd_path_fwd}"\n'
        f'</Directory>\n'
        f'{pub_marker_end}'
    )

    if re.search(re.escape(pub_marker_start), conf_content):
        # Replace existing publication block
        pattern = re.escape(pub_marker_start) + r'[\s\S]*?' + re.escape(pub_marker_end)
        conf_content = re.sub(pattern, pub_block, conf_content)
    else:
        # Append publication block
        conf_content = conf_content.rstrip() + '\n\n' + pub_block + '\n'

    with open(conf_file, 'w', encoding='utf-8') as f:
        f.write(conf_content)
    print('httpd.conf обновлён')

    # --- Normalize httpd_exe for process matching ---
    if os.path.exists(httpd_exe):
        httpd_exe_norm = os.path.normcase(os.path.normpath(os.path.realpath(httpd_exe)))
    else:
        httpd_exe_norm = os.path.normcase(os.path.normpath(httpd_exe))

    # --- Check port availability ---
    holder_pid = check_port_in_use(port)
    if holder_pid:
        our_proc = get_our_httpd(httpd_exe_norm)
        if not our_proc:
            # Port is held by someone else
            try:
                holder_proc = psutil.Process(holder_pid)
                holder_name = f'{holder_proc.name()} (PID: {holder_pid})'
            except (psutil.NoSuchProcess, psutil.AccessDenied):
                holder_name = f'PID {holder_pid}'
            print(f'Error: порт {port} занят процессом {holder_name}')
            print('Укажите другой порт: -Port 9090')
            sys.exit(1)

    # --- Start Apache if not running ---
    httpd_proc = get_our_httpd(httpd_exe_norm)
    if httpd_proc:
        first_pid = httpd_proc[0].pid
        print(f'Apache уже запущен (PID: {first_pid})')
        print('Перезапуск для применения конфигурации...')
        for p in httpd_proc:
            try:
                p.kill()
            except (psutil.NoSuchProcess, psutil.AccessDenied):
                pass
        time.sleep(1)
    else:
        # Check if a foreign httpd holds the port
        foreign_httpd = get_all_httpd()
        if foreign_httpd:
            print(f'[WARN] Обнаружен сторонний Apache (PID: {foreign_httpd[0].pid})')
            print(f'       Наш Apache: {httpd_exe}')

    print('Запуск Apache...')
    subprocess.Popen(
        [httpd_exe],
        cwd=apache_path,
        creationflags=subprocess.CREATE_NO_WINDOW,
    )

    time.sleep(2)

    httpd_check = get_our_httpd(httpd_exe_norm)
    if httpd_check:
        print(f'Apache запущен (PID: {httpd_check[0].pid})')
    else:
        print('Apache не удалось запустить')
        # Run config test for diagnostics
        try:
            result = subprocess.run(
                [httpd_exe, '-t'],
                capture_output=True,
                text=True,
                timeout=10,
            )
            test_output = (result.stdout + result.stderr).strip()
            if test_output:
                print('--- httpd -t ---')
                for line in test_output.splitlines():
                    print(f'  {line}')
        except Exception:
            pass
        error_log = os.path.join(apache_path, 'logs', 'error.log')
        if os.path.exists(error_log):
            print('--- error.log (последние 10 строк) ---')
            try:
                with open(error_log, 'r', encoding='utf-8-sig', errors='replace') as f:
                    all_lines = f.readlines()
                for line in all_lines[-10:]:
                    print(line.rstrip())
            except Exception:
                pass
        sys.exit(1)

    # --- Result ---
    print('')
    print('=== Публикация готова ===')
    print(f'URL:          http://localhost:{port}/{app_name}')
    print(f'OData:        http://localhost:{port}/{app_name}/odata/standard.odata')
    print(f'HTTP-сервисы: http://localhost:{port}/{app_name}/hs/<RootUrl>/...')
    print(f'Web-сервисы:  http://localhost:{port}/{app_name}/ws/<Имя>?wsdl')


if __name__ == '__main__':
    main()
