"""Synthetic local transaction example. No network, model or external side effect."""
import json
import sqlite3
import tempfile
from contextlib import closing
from pathlib import Path


class Conflict(ValueError):
    pass


class ResponseLost(ConnectionError):
    pass


class Store:
    def __init__(self, path):
        self.path = str(path)
        with closing(sqlite3.connect(self.path)) as db, db:
            db.executescript('''
                CREATE TABLE IF NOT EXISTS accounts (
                    tenant TEXT NOT NULL, account TEXT NOT NULL,
                    balance INTEGER NOT NULL DEFAULT 0, version INTEGER NOT NULL DEFAULT 0,
                    PRIMARY KEY (tenant, account));
                CREATE TABLE IF NOT EXISTS operations (
                    tenant TEXT NOT NULL, operation_key TEXT NOT NULL,
                    payload TEXT NOT NULL, result TEXT NOT NULL,
                    PRIMARY KEY (tenant, operation_key));
            ''')
            for tenant in ('alpha', 'beta'):
                db.execute('INSERT OR IGNORE INTO accounts (tenant, account) VALUES (?, ?)', (tenant, 'account-1'))

    def apply(self, tenant, operation_key, account, amount, lose_response=False):
        if not tenant or not operation_key or type(amount) is not int or amount <= 0:
            raise ValueError('A scoped operation key and positive integer amount are required')
        payload = json.dumps({'account': account, 'amount': amount}, sort_keys=True)
        db = sqlite3.connect(self.path, timeout=10, isolation_level=None)
        try:
            db.execute('BEGIN IMMEDIATE')
            previous = db.execute('SELECT payload, result FROM operations WHERE tenant=? AND operation_key=?', (tenant, operation_key)).fetchone()
            if previous:
                if previous[0] != payload:
                    raise Conflict('The operation key is already bound to another payload')
                result = json.loads(previous[1])
            else:
                current = db.execute('SELECT balance, version FROM accounts WHERE tenant=? AND account=?', (tenant, account)).fetchone()
                if current is None:
                    raise ValueError('Account not found in this tenant')
                result = {'account': account, 'balance': current[0] + amount, 'version': current[1] + 1}
                db.execute('UPDATE accounts SET balance=?, version=? WHERE tenant=? AND account=?', (result['balance'], result['version'], tenant, account))
                db.execute('INSERT INTO operations VALUES (?, ?, ?, ?)', (tenant, operation_key, payload, json.dumps(result, sort_keys=True)))
            db.execute('COMMIT')
        except Exception:
            if db.in_transaction:
                db.execute('ROLLBACK')
            raise
        finally:
            db.close()
        if lose_response:
            raise ResponseLost('Commit succeeded; response was intentionally lost')
        return result

    def counts(self):
        with closing(sqlite3.connect(self.path)) as db:
            return {'operations': db.execute('SELECT count(*) FROM operations').fetchone()[0],
                    'alpha_balance': db.execute('SELECT balance FROM accounts WHERE tenant=?', ('alpha',)).fetchone()[0],
                    'beta_balance': db.execute('SELECT balance FROM accounts WHERE tenant=?', ('beta',)).fetchone()[0]}


def scenario():
    with tempfile.TemporaryDirectory() as directory:
        store = Store(Path(directory) / 'fixture.db')
        try:
            store.apply('alpha', 'op-42', 'account-1', 5, lose_response=True)
        except ResponseLost:
            pass
        recovered = store.apply('alpha', 'op-42', 'account-1', 5)
        try:
            store.apply('alpha', 'op-42', 'account-1', 8)
            conflict = False
        except Conflict:
            conflict = True
        before_second_tenant = store.counts()
        store.apply('beta', 'op-42', 'account-1', 7)
        return {'fixture': 'synthetic local SQLite transaction', 'recovered_result': recovered,
                'conflicting_payload_rejected': conflict, 'after_retry': before_second_tenant,
                'after_distinct_tenant': store.counts()}


if __name__ == '__main__':
    print(json.dumps(scenario(), indent=2, sort_keys=True))
