#!/usr/bin/env python3
"""
Универсальный генератор полного Allure-отчёта с накопленной историей.

Использование:
    python3 generate_full.py -i v1.6 -o full-v1.6
    python3 generate_full.py -i v2.0 -o full-v2.0
    python3 generate_full.py -i v1.6,v2.0 -o full
    python3 generate_full.py              # авто-сканирование всех вариантов
"""

import argparse
import hashlib
import json
import shutil
import subprocess
import sys
from pathlib import Path

DEFAULT_BASE = Path('/opt/allure_reports')
EXCLUDED_DIRS = {'cache', 'scripts'}
EXCLUDED_PREFIXES = ('full-',)
DEFAULT_OUTPUT = 'full'

TMP_RESULTS = Path('/tmp/allure-gen-results')
TMP_OUT = Path('/tmp/allure-gen-out')

DOCKER_IMAGE = 'frankescobar/allure-docker-service'


def parse_args():
    parser = argparse.ArgumentParser(
        description='Generate full Allure report with accumulated history'
    )
    parser.add_argument(
        '-b', '--base',
        default=str(DEFAULT_BASE),
        help=f'Base directory (default: {DEFAULT_BASE})'
    )
    parser.add_argument(
        '-i', '--input',
        default=None,
        help='Variant(s) via comma, e.g. "v1.6" or "v1.6,v2.0". '
             'If omitted — auto-scan all variants.'
    )
    parser.add_argument(
        '-o', '--output',
        default=DEFAULT_OUTPUT,
        help=f'Output folder name inside base dir (default: {DEFAULT_OUTPUT})'
    )
    return parser.parse_args()


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def is_timestamp_dir(name: str, excluded_dirs: set, excluded_prefixes: tuple) -> bool:
    """Return True if directory name looks like a timestamp run folder."""
    if name in excluded_dirs:
        return False
    if any(name.startswith(p) for p in excluded_prefixes):
        return False
    return True


def scan_variants(base: Path) -> list:
    """
    Auto-discover all unique variant names that have allure-results subfolder
    across all timestamp directories.
    Returns sorted list of variant names (e.g. ['v1.6', 'v2.0']).
    """
    variants = set()
    for entry in base.iterdir():
        if not entry.is_dir():
            continue
        if not is_timestamp_dir(entry.name, EXCLUDED_DIRS, EXCLUDED_PREFIXES):
            continue
        for sub in entry.iterdir():
            if sub.is_dir() and (sub / 'allure-results').exists():
                variants.add(sub.name)
    return sorted(variants)


def scan_runs(base: Path, variants: list, output_name: str) -> list:
    """
    Scan base for timestamp directories that contain at least one of the
    given variants with allure-results subfolder.
    Returns sorted list of timestamp names (run names).
    """
    # output_name itself must not appear as a run
    runs = []
    for entry in base.iterdir():
        if not entry.is_dir():
            continue
        if not is_timestamp_dir(entry.name, EXCLUDED_DIRS, EXCLUDED_PREFIXES):
            continue
        # Check at least one variant has allure-results
        has_any = False
        for v in variants:
            if (entry / v / 'allure-results').exists():
                has_any = True
                break
        if has_any:
            runs.append(entry.name)
    runs.sort()
    return runs


def cache_key(variants: list, runs: list) -> str:
    """Compute sha1 cache key from variants and runs list."""
    payload = json.dumps(
        {'variants': sorted(variants), 'runs': runs},
        separators=(',', ':')
    )
    return hashlib.sha1(payload.encode('utf-8')).hexdigest()


def load_cache_index(cache_dir: Path) -> list:
    """Load cache index.json, return list of entries or empty list."""
    index_path = cache_dir / 'index.json'
    if not index_path.exists():
        return []
    try:
        with open(index_path) as f:
            return json.load(f)
    except (json.JSONDecodeError, IOError):
        return []


def save_cache_index(cache_dir: Path, entries: list):
    """Save cache index.json."""
    cache_dir.mkdir(parents=True, exist_ok=True)
    with open(cache_dir / 'index.json', 'w') as f:
        json.dump(entries, f, indent=2)


def find_best_cache_entry(entries: list, variants: list, desired_runs: list):
    """
    Find best matching cache entry where:
      - entry['variants'] == sorted(variants)
      - entry['runs'] is a valid prefix of desired_runs
    Sort by len(runs) descending, return first match.
    Returns (entry, cached_count) or (None, 0).
    """
    sv = sorted(variants)
    sorted_entries = sorted(entries, key=lambda e: len(e.get('runs', [])), reverse=True)
    for entry in sorted_entries:
        if entry.get('variants') != sv:
            continue
        entry_runs = entry.get('runs', [])
        n = len(entry_runs)
        if n == 0:
            continue
        if n > len(desired_runs):
            continue
        if desired_runs[:n] == entry_runs:
            return entry, n
    return None, 0


def copy_results(results_src: Path, dst: Path):
    """Copy allure-results to dst, skipping history/ subfolder."""
    dst.mkdir(parents=True, exist_ok=True)
    for f in results_src.iterdir():
        if f.is_file():
            shutil.copy2(f, dst / f.name)
        elif f.is_dir() and f.name != 'history':
            target = dst / f.name
            if target.exists():
                shutil.rmtree(target)
            shutil.copytree(f, target)


def inject_history(history_src: Path, results_dst: Path):
    """Copy history from history_src into results_dst/history/."""
    hist_dst = results_dst / 'history'
    if hist_dst.exists():
        shutil.rmtree(hist_dst)
    shutil.copytree(history_src, hist_dst)
    hist_files = list(hist_dst.iterdir())
    print(f'  Injected history: {len(hist_files)} files')


def run_allure_generate():
    """Run allure generate via docker. Returns True on success."""
    result = subprocess.run([
        'docker', 'run', '--rm',
        '-v', f'{TMP_RESULTS}:/allure-results',
        '-v', f'{TMP_OUT}:/allure-report',
        DOCKER_IMAGE,
        'allure', 'generate', '/allure-results', '-o', '/allure-report', '--clean'
    ], capture_output=True, text=True)

    if result.returncode != 0:
        print(f'  ERROR returncode={result.returncode}')
        if result.stdout:
            print(f'  STDOUT: {result.stdout[:500]}')
        if result.stderr:
            print(f'  STDERR: {result.stderr[:500]}')
        return False
    return True


def save_history_to_cache(cache_dir: Path, variants: list, runs_so_far: list) -> Path:
    """
    Save history from TMP_OUT/history/ to cache.
    Returns path to saved history dir, or None if no history in output.
    """
    out_history = TMP_OUT / 'history'
    if not out_history.exists():
        print('  WARNING: no history/ in output, skipping cache save')
        return None

    key = cache_key(variants, runs_so_far)
    history_cache_dir = cache_dir / key / 'history'
    if history_cache_dir.exists():
        shutil.rmtree(history_cache_dir)
    history_cache_dir.parent.mkdir(parents=True, exist_ok=True)
    shutil.copytree(out_history, history_cache_dir)
    hist_files = list(history_cache_dir.iterdir())
    print(f'  Saved history to cache: {key[:12]}... ({len(hist_files)} files)')
    return history_cache_dir


def update_cache_index(cache_dir: Path, entries: list, variants: list, runs_so_far: list):
    """Add new entry to index if not already present."""
    sv = sorted(variants)
    key = cache_key(variants, runs_so_far)
    for entry in entries:
        if entry.get('variants') == sv and entry.get('runs') == runs_so_far:
            return  # Already exists
    entries.append({
        'variants': sv,
        'runs': runs_so_far,
        'path': key,
    })
    save_cache_index(cache_dir, entries)


def patch_trend(output_dir: Path, runs: list):
    """
    Patch history-trend.json in output dir:
      trend[i]['buildOrder'] = i + 1   (1-based, from old to new)
      trend[i]['reportName'] = runs[len(runs) - 1 - i]  (trend goes new→old)
    Patches both widgets/history-trend.json and history/history-trend.json.
    """
    paths = [
        output_dir / 'widgets' / 'history-trend.json',
        output_dir / 'history' / 'history-trend.json',
    ]
    trend = []
    for path in paths:
        if not path.exists():
            print(f'  WARNING: {path} not found, skipping patch')
            continue
        with open(path) as f:
            trend = json.load(f)
        for i, item in enumerate(trend):
            run_idx = len(runs) - 1 - i
            item['buildOrder'] = run_idx + 1
            item['reportName'] = runs[run_idx]
        with open(path, 'w') as f:
            json.dump(trend, f, indent=2)
        print(f'  Patched: {path}')
    if trend:
        print(f'  Trend points: {len(trend)}')
        for t in trend[:5]:
            print(f'    [{t["buildOrder"]:2d}] {t.get("reportName", "?")} total={t["data"].get("total", "?")}')


def print_stats(output_dir: Path, runs: list):
    """Print final statistics."""
    print('\n=== STATISTICS ===')
    summary_path = output_dir / 'widgets' / 'summary.json'
    if summary_path.exists():
        with open(summary_path) as f:
            summary = json.load(f)
        stat = summary.get('statistic', {})
        total = stat.get('total', 0)
        passed = stat.get('passed', 0)
        failed = stat.get('failed', 0)
        broken = stat.get('broken', 0)
        skipped = stat.get('skipped', 0)
        print(f'Total tests: {total} (passed={passed}, failed={failed}, broken={broken}, skipped={skipped})')
    else:
        print('WARNING: summary.json not found')

    trend_path = output_dir / 'widgets' / 'history-trend.json'
    if trend_path.exists():
        with open(trend_path) as f:
            trend = json.load(f)
        print(f'Trend points: {len(trend)}')
    else:
        print('WARNING: history-trend.json not found')

    hist_path = output_dir / 'history' / 'history.json'
    if hist_path.exists():
        with open(hist_path) as f:
            hist = json.load(f)
        print(f'Tests with history: {len(hist)}')
    else:
        print('WARNING: history/history.json not found')


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main():
    args = parse_args()
    base = Path(args.base)
    output_name = args.output

    print('=== Allure Full Report Generator ===')
    print(f'Base dir     : {base}')

    # --- Защита: base должна существовать ---
    if not base.exists() or not base.is_dir():
        print(f'ERROR: Base directory does not exist: {base}')
        sys.exit(1)

    # --- Защита: абсолютный путь в -i ---
    if args.input and args.input.startswith('/'):
        print('ERROR: Absolute path is not supported for --input.')
        print(f'  Got: {args.input}')
        print('  Use variant names like: v1.6  or  v1.6,v2.0')
        sys.exit(1)

    # --- Определяем варианты ---
    if args.input:
        variants = [v.strip() for v in args.input.split(',') if v.strip()]
    else:
        variants = scan_variants(base)
        if not variants:
            print(f'ERROR: No variants with allure-results found in {base}')
            sys.exit(1)
        print(f'Auto-detected variants: {variants}')

    print(f'Variants     : {variants}')
    print(f'Output folder: {output_name}')

    # --- Защита: output не должен совпадать с папкой прогона ---
    desired_runs_check = scan_runs(base, variants, output_name)
    if output_name in desired_runs_check:
        print(f'ERROR: Output name "{output_name}" matches an existing run timestamp folder!')
        print('  Refusing to overwrite a run directory.')
        sys.exit(1)

    # --- Сканируем прогоны ---
    desired_runs = desired_runs_check
    if not desired_runs:
        print(f'ERROR: No runs found for variants {variants} in {base}')
        sys.exit(1)
    print(f'\nFound {len(desired_runs)} runs: {desired_runs}')

    # --- Кэш ---
    cache_dir = base / 'cache' / output_name
    cache_dir.mkdir(parents=True, exist_ok=True)
    entries = load_cache_index(cache_dir)
    print(f'Cache dir: {cache_dir} ({len(entries)} entries)')

    # --- Найти лучший кэш ---
    best_entry, cached_count = find_best_cache_entry(entries, variants, desired_runs)

    injected_history = None
    runs_done = []

    if best_entry:
        cached_history_dir = cache_dir / best_entry['path'] / 'history'
        if cached_history_dir.exists():
            injected_history = cached_history_dir
            runs_done = best_entry['runs']
            print(f'Cache HIT: reusing {cached_count} runs from {best_entry["path"][:12]}...')
        else:
            print('WARNING: Cache entry found but history dir missing, starting from scratch')
            cached_count = 0
    else:
        print('Cache MISS: generating from scratch')

    # --- Остаточные прогоны ---
    remaining_runs = desired_runs[cached_count:]

    if not remaining_runs:
        print('All runs already cached! Re-running last run to produce final output...')
        remaining_runs = [desired_runs[-1]]
        runs_done = desired_runs[:-1]
        if runs_done:
            prev_entry, _ = find_best_cache_entry(entries, variants, runs_done)
            if prev_entry:
                cached_history_dir = cache_dir / prev_entry['path'] / 'history'
                injected_history = cached_history_dir if cached_history_dir.exists() else None
            else:
                injected_history = None
        else:
            injected_history = None

    print(f'\nRuns to process: {len(remaining_runs)}')
    total_to_process = len(remaining_runs)

    for step_i, run in enumerate(remaining_runs, 1):
        print(f'\n[{step_i}/{total_to_process}] Processing {run}...')

        # Подготовить временные папки
        shutil.rmtree(TMP_RESULTS, ignore_errors=True)
        shutil.rmtree(TMP_OUT, ignore_errors=True)
        TMP_RESULTS.mkdir(parents=True, exist_ok=True)
        TMP_OUT.mkdir(parents=True, exist_ok=True)
        TMP_OUT.chmod(0o777)
        TMP_RESULTS.chmod(0o777)

        # Скопировать results из всех вариантов (слияние)
        copied_any = False
        for v in variants:
            results_src = base / run / v / 'allure-results'
            if results_src.exists():
                print(f'  Merging: {results_src}')
                copy_results(results_src, TMP_RESULTS)
                copied_any = True

        if not copied_any:
            print(f'  SKIP: no allure-results found for any variant in {run}')
            continue

        # Инжектировать историю если есть
        if injected_history and injected_history.exists():
            inject_history(injected_history, TMP_RESULTS)

        # Запустить allure generate
        success = run_allure_generate()
        if not success:
            print(f'FATAL: allure generate failed on run {run}')
            sys.exit(1)
        print('  OK')

        # Обновить runs_done и сохранить историю в кэш
        runs_so_far = runs_done + remaining_runs[:step_i]
        history_cache_dir = save_history_to_cache(cache_dir, variants, runs_so_far)

        if history_cache_dir:
            update_cache_index(cache_dir, entries, variants, runs_so_far)
            injected_history = history_cache_dir

    # --- Скопировать финальный отчёт ---
    final_dir = base / output_name
    print(f'\nCopying final report to {final_dir}...')
    if final_dir.exists():
        shutil.rmtree(final_dir)
    shutil.copytree(TMP_OUT, final_dir)

    # --- Патч тренда ---
    print('\n=== PATCHING TREND ===')
    patch_trend(final_dir, desired_runs)

    # --- chown ---
    subprocess.run(['chown', '-R', 'www-data:www-data', str(final_dir)], check=False)
    print(f'\nchown www-data applied to {final_dir}')

    # --- Статистика ---
    print_stats(final_dir, desired_runs)

    print('\n=== DONE ===')
    print(f'Report: {final_dir}')


if __name__ == '__main__':
    main()
