"""InFinBot file, Decimal and evidence primitives; no trading endpoints."""
from contextlib import contextmanager
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
import hashlib
import json
import os
from pathlib import Path
import sqlite3
import stat
import tempfile


class Rejected(ValueError):
    pass


def number(value, positive=False):
    if type(value) not in (str, int, Decimal):
        raise Rejected('Use finite decimal strings or integers; no float or boolean')
    try:
        result = Decimal(value)
    except InvalidOperation:
        raise Rejected('Invalid decimal') from None
    if not result.is_finite() or abs(result)>Decimal('1e20') or (positive and result<=0):
        raise Rejected('Finite supported amount required')
    return result


def instant(value):
    result = datetime.fromisoformat(value.replace('Z', '+00:00'))
    if result.utcoffset() is None:
        raise Rejected('Timezone-aware timestamp required')
    return result.astimezone(timezone.utc)


def timestamp():
    return datetime.now(timezone.utc).isoformat()


def pairs(items):
    result={}
    for key,value in items:
        if key in result: raise Rejected('Duplicate JSON field')
        result[key]=value
    return result


def strict_json(raw):
    if len(raw)>1024*1024: raise Rejected('Oversized JSON')
    return json.loads(raw, object_pairs_hook=pairs, parse_float=str,
                      parse_constant=lambda _: (_ for _ in ()).throw(Rejected('Nonfinite JSON')))


def private_dir(path):
    path=Path(path).absolute()
    if path.is_symlink(): raise Rejected('Symlink directory rejected')
    path.mkdir(parents=True, exist_ok=True, mode=0o700)
    st=path.stat()
    if st.st_uid!=os.getuid() or st.st_mode&0o077: raise Rejected('Owner-private mode-700 directory required')
    return path


def private_read(path, limit=1024*1024):
    fd=os.open(str(path),os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK)
    with os.fdopen(fd,'rb') as f:
        st=os.fstat(f.fileno())
        if not stat.S_ISREG(st.st_mode) or st.st_uid!=os.getuid() or st.st_mode&0o077 or st.st_nlink!=1:
            raise Rejected('Owner-private regular mode-600/400 file required')
        raw=f.read(limit+1)
    if len(raw)>limit: raise Rejected('Oversized private file')
    return raw


def atomic_json(path,payload,replace=False):
    path=Path(path).absolute();private_dir(path.parent)
    if path.exists() and (path.is_symlink() or path.stat().st_mode&0o077): raise Rejected('Unsafe existing output')
    fd,tmp=tempfile.mkstemp(prefix='.infinbot-',dir=path.parent)
    try:
        with os.fdopen(fd,'w') as f:
            os.fchmod(f.fileno(),0o600)
            json.dump(payload,f,indent=2,allow_nan=False);f.write('\n');f.flush();os.fsync(f.fileno())
        if replace:os.replace(tmp,path)
        else:os.link(tmp,path)
    finally:
        if os.path.exists(tmp):os.unlink(tmp)


@contextmanager
def readonly_db(path):
    p=Path(path).absolute()
    if not p.is_file() or p.is_symlink():raise Rejected('Existing regular current ledger required')
    c=sqlite3.connect(p.as_uri()+'?mode=ro',uri=True,timeout=3)
    c.row_factory=sqlite3.Row;c.execute('PRAGMA query_only=ON')
    try:
        c.execute('BEGIN')
        if [r[0] for r in c.execute('PRAGMA quick_check')]!=['ok']:raise Rejected('Ledger integrity check failed')
        yield c
    finally:c.rollback();c.close()


def digest(path):
    h=hashlib.sha256()
    with Path(path).open('rb') as f:
        for block in iter(lambda:f.read(1024*1024),b''):h.update(block)
    return h.hexdigest()
