"""Create a local HMAC snapshot; never emit configuration values or key bytes."""
import argparse
import hashlib
import hmac
import json
import os
import re
from pathlib import Path
from datetime import datetime, timezone


def snapshot(values, key, environment):
    if not isinstance(values, dict) or len(values) > 3000:
        raise ValueError('Configuration must be a JSON object with at most 3000 keys.')
    if len(key) != 32:
        raise ValueError('Fingerprint key must contain 32 bytes.')
    if not environment or len(environment) > 200:
        raise ValueError('Supply an environment name.')
    entries = []
    for name, value in sorted(values.items()):
        if not isinstance(name, str) or not name or len(name) > 300:
            raise ValueError('Invalid configuration entry name.')
        encoded = json.dumps([name, value], sort_keys=True, separators=(',', ':'), ensure_ascii=False, allow_nan=False).encode()
        entries.append({'name': name, 'fingerprint': hmac.new(key, encoded, hashlib.sha256).hexdigest()})
    return {'environment': environment, 'keyId': hashlib.sha256(key).hexdigest(), 'capturedAt': datetime.now(timezone.utc).isoformat(), 'entries': entries}


def unique_object(pairs):
    values = {}
    for name, value in pairs:
        if name in values:
            raise ValueError('Duplicate configuration key.')
        values[name] = value
    return values


def read_bounded(path, limit):
    # Read through one descriptor; a separate stat/read can race file growth.
    with open(path, 'rb') as handle:
        data = handle.read(limit + 1)
    if len(data) > limit:
        raise ValueError('Input exceeds its allowed size.')
    return data


def comparison(baseline, current):
    if not isinstance(baseline, dict) or set(baseline) - {'environment', 'keyId', 'capturedAt', 'entries'}:
        raise ValueError('Expected a fingerprint snapshot, not raw configuration.')
    if baseline.get('environment') != current['environment'] or baseline.get('keyId') != current['keyId']:
        raise ValueError('Baseline environment and fingerprint key must match.')
    entries = baseline.get('entries')
    if not isinstance(entries, list) or len(entries) > 3000:
        raise ValueError('Invalid baseline entries.')
    names = set()
    for entry in entries:
        if not isinstance(entry, dict) or set(entry) != {'name', 'fingerprint'}:
            raise ValueError('Baseline must only contain names and fingerprints.')
        name = entry['name']
        if not isinstance(name, str) or not name or len(name) > 300 or name in names:
            raise ValueError('Invalid or duplicate baseline name.')
        if not isinstance(entry['fingerprint'], str) or not re.fullmatch(r'[a-f0-9]{64}', entry['fingerprint']):
            raise ValueError('Invalid baseline fingerprint.')
        names.add(name)
    # Only copy validated fields. Never copy arbitrary metadata into evidence.
    clean = {key: baseline[key] for key in ('environment', 'keyId', 'entries')}
    return {'baseline': clean, 'current': current}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--input', required=True, help='Local JSON configuration map; never uploaded')
    parser.add_argument('--key', required=True, help='Private local fingerprint key file')
    parser.add_argument('--baseline', help='Existing fingerprint snapshot to compare against; never changed')
    parser.add_argument('--create-key', action='store_true', help='Create a new private key; fails if the file exists')
    parser.add_argument('--environment', required=True)
    parser.add_argument('--output', required=True)
    args = parser.parse_args()
    sources = {Path(args.input).resolve(), Path(args.key).resolve()}
    if args.baseline:
        sources.add(Path(args.baseline).resolve())
    if Path(args.output).resolve() in sources:
        parser.error('Output must not overwrite input or the private key.')
    values = json.loads(read_bounded(args.input, 2_000_000), object_pairs_hook=unique_object)
    if args.create_key:
        descriptor = os.open(args.key, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
        with os.fdopen(descriptor, 'wb') as handle:
            handle.write(os.urandom(32))
    result = snapshot(values, read_bounded(args.key, 32), args.environment)
    if args.baseline:
        baseline = json.loads(read_bounded(args.baseline, 2_000_000), object_pairs_hook=unique_object)
        result = comparison(baseline, result)
    descriptor = os.open(args.output, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
    with os.fdopen(descriptor, 'w') as handle:
        json.dump(result, handle, indent=2)
    print('Fingerprint snapshot saved. Keep the key private and reuse it for comparisons.')


if __name__ == '__main__':
    try:
        main()
    except (ValueError, OSError):
        raise SystemExit('Snapshot failed. Check the JSON, file permissions and paths. Existing files were not overwritten.')
