import re

def load(p):
    raw = open(p, encoding='utf-8', newline='').read()
    return raw.replace('\r\n', '\n'), '\r\n' in raw

def save(p, s, crlf):
    open(p, 'w', encoding='utf-8', newline='').write(s.replace('\n', '\r\n') if crlf else s)

def once(s, old, new):
    assert s.count(old) == 1, (old[:80], s.count(old))
    return s.replace(old, new)

def after_line_in_literals(s, opener, field, line, expect):
    out, pos, n = [], 0, 0
    for m in re.finditer(re.escape(opener), s):
        if m.start() < pos or s[max(0, m.start() - 7):m.start()] == 'struct ':
            continue
        fm = re.compile(r'\n([ \t]*)' + re.escape(field) + r'[^\n]*').search(s, m.end())
        close = s.find('\n', s.find('}', m.end()))
        assert fm and fm.start() < close, (opener, s[m.end():m.end() + 200])
        out.append(s[pos:fm.end()])
        out.append('\n' + fm.group(1) + line)
        pos = fm.end()
        n += 1
    out.append(s[pos:])
    assert n == expect, (opener, n, expect)
    return ''.join(out)

# relcache: the field
p = 'crates/spt-daemon/src/relcache.rs'
s, c = load(p)
s = once(s, '''    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub current_source: Option<String>,
}''', '''    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub current_source: Option<String>,
    /// When the CURRENT bytes were installed (epoch ms), written with
    /// `current_source` and only by an install or update that retains new
    /// bytes. Not the registry record's `registered_at_ms`: re-adding the same
    /// `<path>` re-registers with no new bytes, and a pointer-mode source moves
    /// its bytes with no re-register at all, so that stamp can say either.
    // [impl->REQ-DOCS-ADAPTER-PAGE]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub current_at_ms: Option<u64>,
}''')
# literals: after current_source line
s = after_line_in_literals(s, 'RetainedAdapter {\n            kind', 'current_source:', 'current_at_ms: None,', 1)
s = once(s, '''        meta.current_source = Some("release".into());
        cache.retain_adapter("a", b"SPT", &meta).unwrap();
        assert_eq!(cache.retained_adapter("a").unwrap().current_source.as_deref(), Some("release"));''',
'''        meta.current_source = Some("release".into());
        meta.current_at_ms = Some(1_790_000_000_000);
        cache.retain_adapter("a", b"SPT", &meta).unwrap();
        let back = cache.retained_adapter("a").unwrap();
        assert_eq!(back.current_source.as_deref(), Some("release"));
        assert_eq!(back.current_at_ms, Some(1_790_000_000_000));''')
s = once(s, '''        assert_eq!(parsed.current_source, None);''', '''        assert_eq!(parsed.current_source, None);
        assert_eq!(parsed.current_at_ms, None);''')
save(p, s, c)

for p in ['crates/spt/tests/adapter_peer_e2e.rs', 'crates/spt/tests/source_ladder_e2e.rs']:
    s, c = load(p)
    s = after_line_in_literals(s, 'RetainedAdapter {', 'current_source:', 'current_at_ms: None,', 1)
    save(p, s, c)

p = 'crates/spt/src/cli.rs'
s, c = load(p)
s = after_line_in_literals(s, 'spt_daemon::relcache::RetainedAdapter {\n', 'current_source', 'current_at_ms: Some(now_ms()),', 2)
save(p, s, c)
# the test seed got Some(now_ms()) too; make the seed None
s, c = load(p)
idx = s.index('let seeded = spt_daemon::relcache::RetainedAdapter {')
j = s.index('current_at_ms: Some(now_ms()),', idx)
s = s[:j] + 'current_at_ms: None,' + s[j + len('current_at_ms: Some(now_ms()),'):]
save(p, s, c)

p = 'crates/spt-daemon/tests/docs_server_e2e.rs'
s, c = load(p)
s = once(s, '''        current_source: Some("subnet".into()),
    };''', '''        current_source: Some("subnet".into()),
        current_at_ms: Some(1_790_000_000_000),
    };''')
s = once(s, '''    assert!(page.contains("v2.1.0 · installed "), "{page}");''', '''    let installed = spt_runtime::surfaces::installed_at_local(1_790_000_000_000).unwrap();
    assert!(page.contains(&format!("v2.1.0 · installed {installed} · via subnet</p>")), "the sidecar's install time: {page}");''')
s = once(s, '''    assert!(!page.contains("via"), "no sidecar, no source named: {page}");''', '''    assert!(!page.contains("via"), "no sidecar, no source named: {page}");
    // Nor an install time: the registry stamp moves on a re-add of the same
    // bytes, so without the sidecar the page says nothing rather than guess.
    assert!(page.contains("<p class=\\"facts\\">v1.0.0</p>"), "{page}");''')
save(p, s, c)

p = 'crates/spt-daemon/src/webserve.rs'
s, c = load(p)
s = once(s, '''    let source = crate::ReleaseCache::open(&home.join("releases"))
        .retained_adapter(adapter)
        .and_then(|sidecar| sidecar.current_source)
        .and_then(|word| AdapterSource::parse(&word));''', '''    // Both halves of "when and from where" come from the retained sidecar,
    // written only when new bytes land; an adapter without one (a `<path>` or
    // `--github` install, or one retained before these fields) names neither.
    let sidecar = crate::ReleaseCache::open(&home.join("releases")).retained_adapter(adapter);
    let source = sidecar
        .as_ref()
        .and_then(|s| s.current_source.as_deref())
        .and_then(AdapterSource::parse);
    let installed = sidecar
        .as_ref()
        .and_then(|s| s.current_at_ms)
        .and_then(surfaces::installed_at_local);''')
s = once(s, '''        installed: surfaces::installed_at_local(record.registered_at_ms),''', '''        installed,''')
save(p, s, c)

p = 'docs/MANIFEST.md'
s, c = load(p)
s = once(s, '''`docs_dir`. Whatever is not declared is named on one dim line. The *via* clause
comes from the retained archive's record and is written on every install and
update. An adapter installed from a `<path>` or `--github`, and any adapter
installed before the record carried it, names no source.''', '''`docs_dir`. Whatever is not declared is named on one dim line. The *installed*
time and the *via* source both come from the retained archive's record, written
only when an install or update lands new bytes. An adapter installed from a
`<path>` or `--github`, and any adapter installed before the record carried
them, names neither: re-adding the same path re-registers without new bytes, so
the registry's own timestamp is not evidence of an install.''')
save(p, s, c)
print('ok')
