#!/usr/bin/env python3
"""
pyver2plain — Convert a Python pyver multi-target spec to a plain py3 spec.

Two-pass approach:
  Pass 1 — resolve_pyver_ifs():
      Use rpm.expandMacro() to evaluate every %if whose condition references
      with_pyver or pyappend.  The correct branch is kept; wrapper lines (%if,
      %else, %endif) are dropped.  Nested blocks are handled recursively.

  Pass 2 — transform_second_pass():
      On the resolved spec:
      - Extract %define py_requires_append body → preamble before %description
      - Remove residual %pyver_package lines
      - Add Obsoletes/Provides for %{name}-py3 to preamble
      - Convert "%files %{?pyappend}" → "%files"
      - Collapse consecutive blank lines
"""

import re
import argparse
import rpm


# ── helpers ───────────────────────────────────────────────────────────────────

def find_endif(lines, start):
    depth = 0
    for i in range(start, len(lines)):
        s = lines[i].strip()
        if re.match(r'^%if\b', s):
            depth += 1
        elif s == '%endif':
            depth -= 1
            if depth == 0:
                return i
    return None


def find_else(lines, if_idx, endif_idx):
    depth = 0
    for i in range(if_idx, endif_idx):
        s = lines[i].strip()
        if re.match(r'^%if\b', s):
            depth += 1
        elif s == '%endif':
            depth -= 1
        elif s == '%else' and depth == 1:
            return i
    return None


def eval_condition(expanded):
    """
    Evaluate a simple RPM %if condition string after macro expansion.
    Handles: "a" == "b", "a" != "b", bare numeric/string values.
    """
    e = expanded.strip()

    m = re.match(r'^"([^"]*)"\s*(==|!=)\s*"([^"]*)"$', e)
    if m:
        l, op, r = m.group(1), m.group(2), m.group(3)
        return (l == r) if op == '==' else (l != r)

    m = re.match(r'^(\S+)\s*(==|!=|<=|>=|<|>)\s*(\S+)$', e)
    if m:
        l, op, r = m.group(1), m.group(2), m.group(3)
        ops = {'==': lambda a, b: a == b, '!=': lambda a, b: a != b,
               '<':  lambda a, b: a < b,  '>':  lambda a, b: a > b,
               '<=': lambda a, b: a <= b, '>=': lambda a, b: a >= b}
        try:
            return ops[op](int(l), int(r))
        except (ValueError, KeyError):
            return ops.get(op, lambda a, b: False)(l, r)

    m = re.match(r'^"([^"]*)"$', e)
    if m:
        return bool(m.group(1))

    try:
        return bool(int(e))
    except ValueError:
        return bool(e)


def read_define_body(lines, define_idx):
    raw = lines[define_idx].rstrip()
    if not raw.endswith('\\'):
        m = re.match(r'%(?:define|global)\s+\w+\s+(.*)', raw)
        return [m.group(1).strip()] if m else []
    body = []
    i = define_idx + 1
    while i < len(lines):
        row = lines[i].rstrip()
        if row.endswith('\\'):
            body.append(row[:-1].rstrip())
        else:
            body.append(row)
            i += 1
            break
        i += 1
    return body


def mark_define_block(lines, define_idx, skip):
    skip.add(define_idx)
    if lines[define_idx].rstrip().endswith('\\'):
        i = define_idx + 1
        while i < len(lines):
            skip.add(i)
            if not lines[i].rstrip().endswith('\\'):
                break
            i += 1


def strip_pyappend(line):
    line = re.sub(r'%\{\?pyappend:[^}]*\}', '', line)
    line = re.sub(r'%\{[?]?pyappend\}', '', line)
    return line


def collapse_blanks(lines):
    out, prev_blank = [], False
    for line in lines:
        blank = line.strip() == ''
        if blank and prev_blank:
            continue
        out.append(line)
        prev_blank = blank
    return out


# ── pass 1: resolve %if blocks via rpm ───────────────────────────────────────

def resolve_pyver_ifs(lines):
    """
    Recursively evaluate every %if whose condition references with_pyver or
    pyappend using rpm.expandMacro().  Keep the matching branch; drop wrapper
    lines.  Non-pyver %if blocks are left intact.
    """
    result = []
    i = 0
    while i < len(lines):
        s = lines[i].strip()
        if re.match(r'^%if\b', s):
            condition = re.sub(r'^%if\s+', '', s, count=1)
            if 'with_pyver' in condition or 'pyappend' in condition:
                endif_i = find_endif(lines, i)
                if endif_i is not None:
                    else_i = find_else(lines, i, endif_i)
                    expanded = rpm.expandMacro(condition)
                    cond_true = eval_condition(expanded)
                    if cond_true:
                        branch = lines[i + 1:else_i] if else_i is not None else lines[i + 1:endif_i]
                    else:
                        branch = lines[else_i + 1:endif_i] if else_i is not None else []
                    result.extend(resolve_pyver_ifs(branch))
                    i = endif_i + 1
                    continue
        result.append(lines[i])
        i += 1
    return result


# ── pass 2: handle %pyver_package and %define py_requires_append ─────────────

def get_package_name(lines):
    for line in lines:
        m = re.match(r'^Name:\s+(\S+)', line)
        if m:
            return m.group(1)
    return None


def transform_second_pass(lines):
    skip = set()
    preamble_extras = []

    i = 0
    while i < len(lines):
        s = lines[i].strip()
        if re.match(r'^%define\s+py_requires_append\b', s):
            preamble_extras.extend(read_define_body(lines, i))
            mark_define_block(lines, i, skip)
        elif re.match(r'^%pyver_package\s*$', s):
            skip.add(i)
        i += 1

    preamble_extras = [strip_pyappend(l) for l in preamble_extras]
    preamble_extras = [l for l in preamble_extras if l.strip()]

    pkg_name = get_package_name(lines)
    py3_name = f'{pkg_name}-py3' if pkg_name else '%{name}-py3'

    already_has_py3 = any(
        re.search(r'Obsoletes:.*-py3\b', lines[i].strip())
        for i in range(len(lines)) if i not in skip
    )
    py3_tags = [] if already_has_py3 else [
        f'Obsoletes:     {py3_name} < %{{?epoch:%{{epoch}}:}}%{{version}}-%{{release}}',
        f'Provides:      {py3_name} = %{{?epoch:%{{epoch}}:}}%{{version}}-%{{release}}',
    ]
    all_extra = preamble_extras + py3_tags

    out = []
    injected = False
    for i, line in enumerate(lines):
        if i in skip:
            continue
        s = line.strip()

        if not injected and re.match(r'^%description\s*$', s):
            if all_extra:
                while out and out[-1].strip() == '':
                    out.pop()
                out.extend(all_extra)
                out.append('')
            injected = True

        if re.match(r'^%files\b', s) and '%{?pyappend}' in s:
            line = re.sub(r'\s*%\{\?pyappend\}', '', line)
            out.append(line)
            continue

        # Strip %{?pyappend:...} inline conditionals from %package/%description/%files lines
        if re.match(r'^%(?:package|description|files)\b', s) and '%{?pyappend' in line:
            line = re.sub(r'%\{\?pyappend:[^}]*\}', '', line)
            line = re.sub(r'%\{[?]?pyappend\}', '', line)
            s = line.strip()

        # Replace %{name}-py3 with hardcoded name in Obsoletes/Provides lines
        if pkg_name and re.match(r'^(Obsoletes|Provides):', s):
            line = line.replace('%{name}-py3', py3_name)

        # Update python3.Xdist BuildRequires to current python version
        if re.match(r'^BuildRequires:', s):
            line = re.sub(r'python3\.\d+dist\b', 'python3.14dist', line)

        out.append(line)

    return '\n'.join(collapse_blanks(out))


# ── entry point ───────────────────────────────────────────────────────────────

def transform(lines):
    resolved = resolve_pyver_ifs(lines)
    return transform_second_pass(resolved)


def main():
    ap = argparse.ArgumentParser(
        description='Convert a Python pyver spec to a plain py3 spec.')
    ap.add_argument('specfile', help='RPM spec file to transform')
    ap.add_argument('-o', '--output', metavar='FILE',
                    help='Output file (default: overwrite input)')
    ap.add_argument('--dry-run', action='store_true',
                    help='Print result to stdout, do not write')
    args = ap.parse_args()

    with open(args.specfile) as f:
        content = f.read()

    result = transform(content.splitlines())
    if not result.endswith('\n'):
        result += '\n'

    if args.dry_run:
        print(result, end='')
        return

    out_path = args.output or args.specfile
    with open(out_path, 'w') as f:
        f.write(result)
    label = f'{args.specfile} → {out_path}' if out_path != args.specfile else out_path
    print(f'Updated: {label}')


if __name__ == '__main__':
    main()
