#!/usr/bin/env python3
"""HanBase256: original byte encoding with pause-separated reading guides.

MIT license. No third-party dependencies. Preserve the alphabet ordering.
Checksum format: append the first byte of SHA-256 of the original bytes.
"""

import argparse
import base64
import hashlib
import hmac
import sys
import unicodedata
from pathlib import Path

# BEGIN GENERATED GOLDEN ALPHABET
# Source: hancode/data/base256-golden.json; run tools/sync_alphabet.py.
ALPHABET_REVISION = 13
ALPHABET_STATUS = 'golden'
ALPHABET_SHA256 = 'cb8eb7211dfba72795bff845fc9bf03c7e99e9fe524896e573d388b141201507'
ALPHABET = '가갈감강개거건검결경고곡골곰공과관광교구국군굴권귀그극근금기길김나난남내너노농누니다단달당대더덕도돈돌동두드들등디라란러레력로론루리린마만말망매머면명모목무문물미민밀바반발방배백버번벌범변병보봄봉부북분불비빈사산살삼상색생서석선설섬성세소속손솔송수순술스승시식신실심아악안알암야약양어언엄에여역연열영오온와왕외요용우운울원월위유육윤은이인일임자작장저적전절점정제조종주준중지직진짐차참창처천철청체초촌총최추축출충치친침카칼커코쿠크키타탄탈태터토통투트티파판팔퍼편평표푸풍피필하학한항해행향허현형호혼홍화환황회후히'
DECODE_ALIASES = {}
# END GENERATED GOLDEN ALPHABET
_INDEX = {ch: i for i, ch in enumerate(ALPHABET)}
_INDEX.update(DECODE_ALIASES)

def _split_encoded(text, checksum=False):
    if not isinstance(text, str):
        raise TypeError("Expected a HanBase256 string")
    body = "".join(unicodedata.normalize("NFC", text).split()).replace("·", "")
    if ":" in body:
        raise ValueError(f"This codec uses the original revision-{ALPHABET_REVISION} alphabet; marked experimental formats are not supported")
    if "+" in body:
        if not checksum or body.count("+") != 1 or body.index("+") != len(body) - 2:
            raise ValueError("A + separator must precede exactly one final checksum syllable; enable checksum verification")
        body = body[:-2] + body[-1]
    if checksum and not body:
        raise ValueError("The checksum syllable is missing")
    return body


def encode(data, *, alphabet=f"revision-{ALPHABET_REVISION}"):
    """Encode bytes using the approved golden alphabet."""
    if not isinstance(data, (bytes, bytearray)):
        raise TypeError('encode expects bytes; use text.encode("utf-8") for text')
    if alphabet != f"revision-{ALPHABET_REVISION}":
        raise ValueError(f"Only the original revision-{ALPHABET_REVISION} alphabet is supported")
    return "".join(ALPHABET[byte] for byte in data)


def decode(text):
    """Decode original HanBase256, including pause-separated reading guides."""
    body = _split_encoded(text)
    try:
        return bytes(_INDEX[ch] for ch in body)
    except KeyError as error:
        raise ValueError(f"{error.args[0]!r} is not in the HanBase256 alphabet") from None


def encode_with_checksum(data, *, alphabet=f"revision-{ALPHABET_REVISION}"):
    """Append the first SHA-256 byte, using the approved golden alphabet."""
    if not isinstance(data, (bytes, bytearray)):
        raise TypeError("encode_with_checksum expects bytes")
    return encode(data + hashlib.sha256(data).digest()[:1], alphabet=alphabet)


def decode_with_checksum(text):
    """Verify a checksum, accepting a + before the final syllable."""
    body = _split_encoded(text, checksum=True)
    checked = decode(body)
    data, checksum = checked[:-1], checked[-1:]
    if not hmac.compare_digest(hashlib.sha256(data).digest()[:1], checksum):
        raise ValueError("Checksum mismatch")
    return data


def format_for_speech(text, *, checksum=False):
    """Put a pause between every syllable without changing the byte mapping.

    Formatting checks the alphabet and separator syntax, but does not verify
    the checksum value. Use decode_with_checksum for verification.
    """
    body = _split_encoded(text, checksum=checksum)
    decode(body)
    if checksum:
        payload = " · ".join(body[:-1])
        return payload + (" " if payload else "") + "+" + body[-1]
    return " · ".join(body)


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    inputs = parser.add_mutually_exclusive_group()
    inputs.add_argument('--text', help='Exact input text, without an added newline')
    inputs.add_argument('--file', type=Path, help='Read a file as raw bytes when encoding, or UTF-8 HanBase256 when decoding')
    parser.add_argument('--decode', action='store_true', help='Decode instead of encode')
    parser.add_argument('--checksum', action='store_true', help='Append a checksum, or require and verify it when decoding')
    parser.add_argument('--sha256', action='store_true', help='Hash the input bytes before encoding')
    parser.add_argument('--input-format', choices=['text', 'hex', 'base64'], default='text')
    parser.add_argument('--output-format', choices=['hex', 'text', 'base64', 'raw'], default='hex', help='Decoded output format')
    parser.add_argument('-o', '--output', type=Path, help='Write output to a file without an added newline')
    parser.add_argument('--speak', action='store_true', help='Pause-separated reading guide, with + before the checksum')
    args = parser.parse_args()
    try:
        if args.decode and args.speak:
            parser.error('--speak is only valid when encoding')
        if args.decode and args.sha256:
            parser.error('--sha256 is only valid when encoding')
        if args.file and args.input_format != 'text':
            parser.error('--file reads raw bytes; do not also set --input-format')
        if args.file:
            data = args.file.read_bytes()
        else:
            raw = args.text if args.text is not None else sys.stdin.read()
            if args.decode or args.input_format == 'text':
                data = raw.encode('utf-8')
            elif args.input_format == 'hex':
                data = bytes.fromhex(raw.removeprefix('0x'))
            else:
                clean = ''.join(raw.split())
                data = base64.b64decode(clean + '=' * (-len(clean) % 4), validate=True)
        if args.decode:
            decoder = decode_with_checksum if args.checksum else decode
            data = decoder(data.decode('utf-8'))
            if args.output_format == 'hex':
                result = data.hex().encode('ascii')
            elif args.output_format == 'base64':
                result = base64.b64encode(data)
            elif args.output_format == 'text':
                result = data.decode('utf-8').encode('utf-8')
            else:
                result = data
        else:
            if args.sha256:
                data = hashlib.sha256(data).digest()
            encoder = encode_with_checksum if args.checksum else encode
            encoded = encoder(data)
            if args.speak:
                encoded = format_for_speech(encoded, checksum=args.checksum)
            result = encoded.encode('utf-8')
        if args.output:
            args.output.write_bytes(result)
        else:
            sys.stdout.buffer.write(result + (b'' if args.decode and args.output_format == 'raw' else b'\n'))
    except (ValueError, TypeError, OSError) as error:
        parser.exit(1, f'Error: {error}\n')


if __name__ == '__main__':
    main()
