#!/usr/bin/env python3
"""Import a snapshot of the kulibri-rewrite repository into frontend-v3 (the "vendor import" of the cut).

It makes the pristine mapped copy only: the rewrite's files, placed by fixed rules, and nothing else. All integration
work (shell, session login, design tokens, ...) is a separate commit on top; see README.md in this folder.

    python tools/rewrite-cut/import-rewrite.py --rewrite <rewrite checkout> --target <frontend-v3 folder>

Run it on the vendor branch. A second run with a newer snapshot updates the copied files, deletes what the rewrite
removed since the last import, and reports what needs hand work. State is kept in frontend-v3/.rewrite-import-manifest.

Line endings: frontend-v3 stores a mix of CRLF and LF files. A copied file that already exists keeps its own style, and
the hashes ignore line endings, so an editor flipping a file between CRLF and LF in the rewrite is neither a change nor
a whole-file diff in the vendor commit.
"""
import argparse
import hashlib
import io
import json
import os
import re
import shutil
import sys

# ── Rules for paths that exist on both sides (decided in the dry run, 6 Oct 2026) ────────────────────────────────────
# The rewrite's file replaces frontend-v3's file.
REWRITE_WINS = {
    'components/WysiwygEditor.vue', 'composables/usePollSync.ts', 'composables/useTheme.ts', 'mappers.ts',
    'pollKeyStore.ts', 'types.ts', 'index.css', '__tests__/mappers.spec.ts',
}
# frontend-v3's file stays (shell). The rewrite's version is not copied; a change in it is reported for hand porting.
V3_WINS = {
    'App.vue', 'main.ts', 'router/index.ts', 'stores/auth.ts', 'i18n/index.ts', 'links.ts', 'utils/sanitize.ts',
    '__tests__/links.spec.ts', '__tests__/sanitize.spec.ts',
}
# The rewrite's file comes in under another name (same subject, different module).
RENAME_INCOMING = {'__tests__/http.spec.ts': '__tests__/rwHttp.spec.ts'}
# frontend-v3's file moves out of the way so the rewrite's file of the same name can come in.
RENAME_V3 = {'__tests__/fixtures.ts': '__tests__/fixturesV3.ts'}
V3_SPECS_USING_FIXTURES = ['dashboard.spec.ts', 'DashboardView.spec.ts', 'PollCard.spec.ts']
# Moved and rewritten on the way (the rewrite's api.ts is the newer copy of frontend-v3's api/endpoints.ts).
API_MOVE = ('api.ts', 'api/endpoints.ts')
API_IMPORTS = {"from './http'": "from '../http'", "from './pollKeyStore'": "from '../pollKeyStore'",
               "from './utils/pagedPoll'": "from '../utils/pagedPoll'"}
# The rewrite's message catalogue becomes a module that frontend-v3's i18n/index.ts merges under its JSON.
MESSAGES_FROM, MESSAGES_TO = 'i18n/index.ts', 'i18n/rewriteMessages.ts'
# Files outside src/ that follow the rewrite.
ROOT_FILES = ['tailwind.config.js']

MANIFEST = '.rewrite-import-manifest'

NUL = bytes([0])
LF = bytes([10])
CRLF = bytes([13, 10])


def is_binary(data):
    return NUL in data


def to_lf(data):
    return data.replace(CRLF, LF)


def sha1(path):
    """Hash of the content, line endings ignored."""
    with open(path, 'rb') as f:
        data = f.read()
    if not is_binary(data):
        data = to_lf(data)
    return hashlib.sha1(data).hexdigest()


def write_text(dst, text):
    """Write text to a file that may already exist, keeping that file's line-ending style."""
    data = to_lf(text.encode('utf-8'))
    if os.path.exists(dst):
        with open(dst, 'rb') as f:
            if CRLF in f.read():
                data = data.replace(LF, CRLF)
    with open(dst, 'wb') as f:
        f.write(data)


def copy_keeping_eol(src, dst):
    """Copy a file; a file that already exists in frontend-v3 keeps its own line-ending style."""
    with open(src, 'rb') as f:
        data = f.read()
    if os.path.exists(dst) and not is_binary(data):
        with open(dst, 'rb') as f:
            existing = f.read()
        if not is_binary(existing):
            data = to_lf(data)
            if CRLF in existing:
                data = data.replace(LF, CRLF)
    with open(dst, 'wb') as f:
        f.write(data)


def rel_files(root):
    out = []
    for d, _, fs in os.walk(root):
        for f in fs:
            out.append(os.path.relpath(os.path.join(d, f), root).replace('\\', '/'))
    return sorted(out)


def read_manifest(path):
    if not os.path.exists(path):
        return {'copied': {}, 'shell': {}}
    with open(path, encoding='utf-8') as f:
        return json.load(f)


def messages_module(source_path):
    text = to_lf(open(source_path, 'rb').read()).decode('utf-8')
    text = text.replace("import { createI18n } from 'vue-i18n';\n", '')
    m = re.search(r"\nconst i18n = createI18n\(\{.*?\}\);\n\nexport \{ i18n \};\n?", text, re.S)
    if not m:
        sys.exit("i18n/index.ts of the rewrite no longer ends in createI18n(...) + export { i18n }: update messages_module()")
    return (text[:m.start()]
            + "\n/** The rewrite's messages (TS objects). Merged with frontend-v3's JSON in ./index.ts. */\nexport { messages };\n")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--rewrite', required=True, help='checkout of the rewrite repository (the snapshot)')
    ap.add_argument('--target', required=True, help='the frontend-v3 folder to import into')
    args = ap.parse_args()

    rw_root = os.path.abspath(args.rewrite)
    v3_root = os.path.abspath(args.target)
    rw_src, v3_src = os.path.join(rw_root, 'src'), os.path.join(v3_root, 'src')
    for p in (rw_src, v3_src):
        if not os.path.isdir(p):
            sys.exit('not a folder: ' + p)

    manifest_path = os.path.join(v3_root, MANIFEST)
    previous = read_manifest(manifest_path)
    new = {'copied': {}, 'shell': {}}
    log = []

    # frontend-v3's fixtures make room for the rewrite's (idempotent)
    for old, newname in RENAME_V3.items():
        src, dst = os.path.join(v3_src, old), os.path.join(v3_src, newname)
        if os.path.exists(src) and not os.path.exists(dst) and old not in previous['copied']:
            shutil.move(src, dst)
            log.append(f'frontend-v3 renamed: {old} -> {newname}')
            for spec in V3_SPECS_USING_FIXTURES:
                p = os.path.join(v3_src, '__tests__', spec)
                if os.path.exists(p):
                    text = open(p, 'rb').read().decode('utf-8')
                    fixed = text.replace("from './fixtures'", "from './fixturesV3'")
                    if fixed != text:
                        open(p, 'wb').write(fixed.encode('utf-8'))
                        log.append(f'frontend-v3 spec import updated: __tests__/{spec}')

    existing = set(rel_files(v3_src))
    review = []
    undecided = []

    for f in rel_files(rw_src):
        src = os.path.join(rw_src, f)
        digest = sha1(src)

        if f == MESSAGES_FROM:
            # the catalogue is both a shell file (frontend-v3 keeps its own index) and the source of rewriteMessages.ts
            dst = os.path.join(v3_src, MESSAGES_TO)
            os.makedirs(os.path.dirname(dst), exist_ok=True)
            write_text(dst, messages_module(src))
            new['copied'][MESSAGES_TO] = digest

        if f in V3_WINS:
            new['shell'][f] = digest
            if previous['shell'].get(f) not in (None, digest):
                review.append(f)
            continue

        if f == API_MOVE[0]:
            target = API_MOVE[1]
        else:
            target = RENAME_INCOMING.get(f, f)

        if (target in existing and target not in previous['copied']
                and f not in REWRITE_WINS and f not in RENAME_INCOMING and f != API_MOVE[0]):
            undecided.append(f)
            continue

        dst = os.path.join(v3_src, target)
        os.makedirs(os.path.dirname(dst), exist_ok=True)
        if f == API_MOVE[0]:
            text = to_lf(open(src, 'rb').read()).decode('utf-8')
            for old, repl in API_IMPORTS.items():
                text = text.replace(old, repl)
            if re.search(r"from '\./", text):
                sys.exit("api.ts has relative imports the script does not know: add them to API_IMPORTS")
            write_text(dst, text)
        else:
            copy_keeping_eol(src, dst)
        new['copied'][target] = digest

    for name in ROOT_FILES:
        src, dst = os.path.join(rw_root, name), os.path.join(v3_root, name)
        if os.path.exists(src):
            copy_keeping_eol(src, dst)
            new['copied']['../' + name] = sha1(src)

    # Static files from the rewrite's public/ folder that its source refers to by absolute path, for example
    # "/kulibri-logo.png" in a view. Without them the build and the tests fail with "Failed to resolve import".
    ref = re.compile(r"""[\s"'`(,]/([A-Za-z0-9_@.\-]+\.(?:png|jpe?g|webp|svg|ico|gif))""")
    wanted = set()
    for f in rel_files(rw_src):
        if f.endswith(('.vue', '.ts', '.css')) and not f.startswith('__tests__/'):
            wanted.update(ref.findall(open(os.path.join(rw_src, f), encoding='utf-8', errors='ignore').read()))
    rw_public, v3_public = os.path.join(rw_root, 'public'), os.path.join(v3_root, 'public')
    for name in sorted(wanted):
        src = os.path.join(rw_public, name)
        if os.path.isfile(src):
            os.makedirs(v3_public, exist_ok=True)
            copy_keeping_eol(src, os.path.join(v3_public, name))
            new['copied']['../public/' + name] = sha1(src)
            log.append('public file copied: ' + name)

    if undecided:
        print('UNDECIDED COLLISIONS: these paths exist in frontend-v3 and are not covered by a rule.')
        print('Add each to REWRITE_WINS or V3_WINS in this script, then run it again:')
        for f in undecided:
            print('   ', f)
        sys.exit(1)

    # files the rewrite removed since the last import
    removed = []
    for target in previous['copied']:
        if target not in new['copied'] and not target.startswith('../'):
            p = os.path.join(v3_src, target)
            if os.path.exists(p):
                os.remove(p)
                removed.append(target)

    with open(manifest_path, 'w', encoding='utf-8', newline='\n') as f:
        json.dump(new, f, indent=1, sort_keys=True)
        f.write('\n')

    # dependencies the rewrite uses that frontend-v3 does not have
    def deps(root):
        p = os.path.join(root, 'package.json')
        return json.load(open(p, encoding='utf-8')).get('dependencies', {}) if os.path.exists(p) else {}
    missing = sorted(set(deps(rw_root)) - set(deps(v3_root)))

    print(f'imported {len(new["copied"])} files, {len(new["shell"])} shell files left to frontend-v3, '
          f'{len(removed)} removed since the last import')
    for line in log:
        print('  ' + line)
    if removed:
        print('removed (the rewrite deleted them):')
        for t in removed:
            print('   ', t)
    if review:
        print('REVIEW, port by hand: the rewrite changed these shell files since the last import:')
        for f in review:
            print('   ', f)
    if missing:
        print('dependencies in the rewrite but not in frontend-v3/package.json (add and run npm install):', ', '.join(missing))


if __name__ == '__main__':
    main()
