#!/usr/bin/env python3
"""Inspect subtitle bytes and explicitly convert readable text to a new UTF-8 file.

Python 3 standard library only. No network, encoding guesses, cue edits, or
overwrite mode. Review the source encoding and text before using convert.
Copyright 2026 BookTranslator. SPDX-License-Identifier: MIT
"""

import argparse
import codecs
import hashlib
import json
from pathlib import Path
import sys
import unicodedata


def digest(data):
    return hashlib.sha256(data).hexdigest()


def report(value):
    # ASCII-safe JSON keeps terminal fonts/encodings out of the diagnosis.
    print(json.dumps(value, ensure_ascii=True, indent=2))


def write_new(path, data):
    with path.open("xb") as output:
        output.write(data)
    if path.read_bytes() != data:
        raise OSError("Output readback differs. Do not use the new file.")


def inspect(source, encoding, line_number):
    data = source.read_bytes()
    result = {
        "file": str(source),
        "bytes": len(data),
        "sha256": digest(data),
        "utf8_bom": data.startswith(codecs.BOM_UTF8),
        "selected_encoding": encoding,
    }
    try:
        data.decode("utf-8", errors="strict")
        result["strict_utf8"] = True
    except UnicodeDecodeError as error:
        result["strict_utf8"] = False
        result["utf8_error"] = {
            "byte_offset": error.start,
            "hex": data[error.start:error.end].hex(" "),
            "reason": error.reason,
        }
    try:
        text = data.decode(encoding, errors="strict")
    except UnicodeDecodeError as error:
        result["selected_decode_ok"] = False
        result["selected_decode_error"] = str(error)
        report(result)
        return 1

    lines = text.splitlines()
    result["selected_decode_ok"] = True
    result["replacement_characters"] = text.count("\ufffd")
    result["physical_lines"] = len(lines)
    result["preview_first_12_lines"] = lines[:12]
    result["note"] = "Decode success is not proof of correct words or valid SRT."
    if line_number is not None:
        if not 1 <= line_number <= len(lines):
            raise ValueError("--line must identify an existing physical line (1-based).")
        line = lines[line_number - 1]
        result["selected_line"] = {
            "number": line_number,
            "text": line,
            "characters": [
                {
                    "character": character,
                    "codepoint": f"U+{ord(character):04X}",
                    "name": unicodedata.name(character, "UNNAMED/CONTROL"),
                }
                for character in line
            ],
        }
    report(result)
    return 0


def backup(source, destination):
    data = source.read_bytes()
    write_new(destination, data)
    report({
        "operation": "byte-for-byte backup",
        "source": str(source),
        "destination": str(destination),
        "bytes": len(data),
        "sha256": digest(data),
        "readback_matches": True,
    })
    return 0


def convert(source, destination, encoding, with_bom):
    data = source.read_bytes()
    text = data.decode(encoding, errors="strict")
    if "\ufffd" in text:
        raise ValueError(
            "Decoded text contains U+FFFD. No output written. Recover or verify "
            "the original text first; this tool does not guess missing characters."
        )
    if text.startswith("\ufeff"):
        raise ValueError(
            "Decoded text starts with U+FEFF. No output written. For a confirmed "
            "UTF-8 BOM source use --from-encoding utf-8-sig. Check other sources separately."
        )
    output_encoding = "utf-8-sig" if with_bom else "utf-8"
    output = text.encode(output_encoding, errors="strict")
    if output.decode(output_encoding, errors="strict") != text:
        raise ValueError("Decoded output does not equal decoded input. No output written.")
    write_new(destination, output)
    report({
        "operation": "explicit encoding conversion; not text recovery",
        "source_encoding": encoding,
        "output_encoding": output_encoding,
        "destination": str(destination),
        "source_sha256": digest(data),
        "output_sha256": digest(output),
        "decoded_text_unchanged": True,
        "readback_matches": True,
        "note": "Verify the words and playback. This does not validate SRT syntax or media sync.",
    })
    return 0


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    commands = parser.add_subparsers(dest="command", required=True)
    check = commands.add_parser("inspect", help="Read only; print strict decode and line details")
    check.add_argument("source", type=Path)
    check.add_argument("--encoding", default="utf-8-sig")
    check.add_argument("--line", type=int, help="Show code points for one physical line, starting at 1")
    copy = commands.add_parser("backup", help="Make a byte-for-byte copy; destination must not exist")
    copy.add_argument("source", type=Path)
    copy.add_argument("destination", type=Path)
    change = commands.add_parser("convert", help="Convert explicitly decoded text to a NEW UTF-8 file")
    change.add_argument("source", type=Path)
    change.add_argument("destination", type=Path)
    change.add_argument("--from-encoding", required=True)
    change.add_argument("--with-bom", action="store_true", help="Add UTF-8 BOM only if destination requires it")
    args = parser.parse_args()
    try:
        if args.command == "inspect":
            return inspect(args.source, args.encoding, args.line)
        if args.command == "backup":
            return backup(args.source, args.destination)
        return convert(args.source, args.destination, args.from_encoding, args.with_bom)
    except (OSError, UnicodeError, LookupError, ValueError) as error:
        report({"error": str(error), "action": "Stopped. Keep the source; do not use a failed output."})
        return 1


if __name__ == "__main__":
    sys.exit(main())
