#!/usr/bin/env python3
"""Validate bounded constructor graphs; ArkTS checks generated calls against real APIs."""
import argparse
import copy
import json
from pathlib import Path
import re
import os
import subprocess

ROOT = Path(__file__).resolve().parents[1]
GRAPHS = ['composition/di.json', 'components/homefeature/di.json']


def generate(graph):
    inputs = graph['inputs']
    nodes = {node['id']: node for node in graph['nodes']}
    if len(nodes) != len(graph['nodes']) or set(nodes) & set(inputs):
        raise ValueError('Duplicate binding')
    for name in [graph['function'], *inputs, *nodes]:
        if not re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', name):
            raise ValueError('Invalid identifier')
    ordered, visiting, done = [], set(), set(inputs)

    def visit(name):
        if name in visiting:
            raise ValueError('Dependency cycle: ' + name)
        if name in done:
            return
        if name not in nodes:
            raise ValueError('Missing dependency: ' + name)
        visiting.add(name)
        node = nodes[name]
        for dependency in node['args']:
            visit(dependency)
        if set(node.get('methodArgs', {})) - set(node['args']):
            raise ValueError('Unknown method dependency')
        ordered.append(node)
        visiting.remove(name)
        done.add(name)

    for name in nodes:
        visit(name)
    result = graph['result']
    if result not in nodes:
        raise ValueError('Missing result binding')
    lines = ['// Generated by scripts/di; do not edit.']
    for module, symbols in graph['imports'].items():
        lines.append('import { ' + ', '.join(symbols) + " } from '" + module + "';")
    args = ', '.join(name + ': ' + kind for name, kind in inputs.items())
    lines.append('export function ' + graph['function'] + '(' + args + '): ' + nodes[result]['type'] + ' {')
    for node in ordered:
        arguments = [arg + ('.' + node['methodArgs'][arg] + '()' if arg in node.get('methodArgs', {}) else '') for arg in node['args']]
        lines.append('  const ' + node['id'] + ' = new ' + node['type'] + '(' + ', '.join(arguments) + ');')
    lines.extend(['  return ' + result + ';', '}'])
    return '\n'.join(lines) + '\n'


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--scope', choices=['all', 'shell', 'components'], default='all')
    parser.add_argument('--test', action='store_true')
    parser.add_argument('--graph', help='One component-owned graph relative to the product root')
    parser.add_argument('--test-compile', action='store_true', help='Check constructor drift against installed HARs with hvigor; run in a disposable generated project')
    args = parser.parse_args()
    graphs = GRAPHS[:1] if args.scope == 'shell' else GRAPHS[1:] if args.scope == 'components' else GRAPHS
    if args.graph:
        graphs = [args.graph]
    for relative in graphs:
        source = ROOT / relative
        source.resolve().relative_to(ROOT)
        graph = json.loads(source.read_text())
        text = generate(graph)
        if args.test:
            for mode, expected in [('missing', 'Missing dependency'), ('cycle', 'Dependency cycle'), ('duplicate', 'Duplicate binding')]:
                broken = copy.deepcopy(graph)
                if mode == 'duplicate':
                    broken['nodes'].append(copy.deepcopy(broken['nodes'][0]))
                else:
                    broken['nodes'][0]['args'] = ['absent' if mode == 'missing' else broken['nodes'][0]['id']]
                try:
                    generate(broken)
                except ValueError as error:
                    if expected not in str(error):
                        raise
                else:
                    raise ValueError('DI negative test unexpectedly passed')
        else:
            output = (source.parent / graph['output']).resolve()
            output.relative_to(ROOT)
            output.parent.mkdir(parents=True, exist_ok=True)
            if not output.exists() or output.read_text() != text:
                output.write_text(text)
    if args.test_compile:
        source = ROOT / GRAPHS[0]
        original = source.read_bytes()
        graph = json.loads(original)
        output = (source.parent / graph['output']).resolve()
        previous = output.read_bytes() if output.exists() else None
        broken = copy.deepcopy(graph)
        # Passing SessionFactory itself instead of its AnalysisService must fail real ArkTS compilation.
        broken['nodes'][0].pop('methodArgs', None)
        command = [os.environ.get('HVIGOR', 'hvigorw'), 'assembleHap', '--mode', 'module',
                   '-p', 'module={{HARMONY_MODULE_NAME}}@default', '--no-daemon']
        try:
            source.write_text(json.dumps(broken))
            result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True)
            diagnostic = result.stdout + result.stderr
            if result.returncode == 0 or not all(word in diagnostic for word in ('SessionFactory', 'AnalysisService', 'not assignable')):
                raise ValueError('ArkTS did not reject constructor drift: ' + diagnostic)
        finally:
            source.write_bytes(original)
            if previous is None:
                output.unlink(missing_ok=True)
            else:
                output.write_bytes(previous)
        subprocess.run(command, cwd=ROOT, check=True)
        print('Real ArkTS constructor drift rejected; valid shell rebuilt')
    print('DI graphs checked'  + (' including missing/cyclic/duplicate bindings' if args.test else ' and generated'))


if __name__ == '__main__':
    main()
