# -*- coding: utf-8 -*-
"""Offline receipt ledger trial. All identities, orders and events are synthetic.

Run: python3 receipt_trial.py
Does not classify text, call AI, authenticate a real user, connect to an order
service, send email, or perform an invoice change. Fixtures supply task types
and a preauthenticated identity; the local fake order adapter checks ownership.
Persistence is in-memory for one sequential run; no concurrency guarantee.
"""
from pathlib import Path
import csv, hashlib, json

HERE = Path(__file__).resolve().parent

def signature(value):
    return hashlib.sha256(json.dumps(value,sort_keys=True,ensure_ascii=False).encode()).hexdigest()

class FakeOrders:
    def __init__(self):
        self.calls = []
        self.access = []

    def fetch_for_user(self, user, order_id):
        # This check is deliberately inside the fake data boundary, not just
        # in the rendered answer or the receipt router.
        if user != 'demo-user':
            self.access.append({'order':order_id,'outcome':'denied'})
            return None, '本人の注文を閲覧できない'
        self.calls.append(order_id)
        if order_id == 'demo-order-fail':
            self.access.append({'order':order_id,'outcome':'failure'})
            return None, '注文情報の取得失敗'
        if order_id != 'demo-order-1':
            self.access.append({'order':order_id,'outcome':'empty'})
            return None, '該当する注文がない'
        self.access.append({'order':order_id,'outcome':'ok'})
        return {'status':'発送済み（架空）'}, None

class Ledger:
    def __init__(self, orders):
        self.orders, self.receipts, self.seen = orders, {}, {}

    def apply(self, event):
        # Validate the entire receipt before any adapter call. A malformed
        # second task must not cause the first task to be fetched without a
        # corresponding receipt being saved.
        if not isinstance(event, dict):
            return '入力形式が不正なため保留'
        if any(not isinstance(event.get(k), str) or not event[k].strip()
               for k in ('id', 'receipt', 'action')):
            return '入力形式が不正なため保留'
        if event['action'] == 'receive':
            tasks = event.get('tasks')
            if not isinstance(tasks, list) or not tasks:
                return '入力形式が不正なため保留'
            for task in tasks:
                if not isinstance(task, dict) or any(
                    not isinstance(task.get(k), str) or not task[k].strip()
                    for k in ('id', 'kind')
                ):
                    return '入力形式が不正なため保留'
                if task['kind'] == 'order' and (
                    not isinstance(task.get('order'), str) or not task['order'].strip()
                    or (task.get('owner') is not None and not isinstance(task['owner'], str))
                ):
                    return '入力形式が不正なため保留'
        elif event['action'] == 'finish':
            if not isinstance(event.get('task'), str) or not event['task'].strip():
                return '入力形式が不正なため保留'
            if event.get('note') is not None and not isinstance(event['note'], str):
                return '入力形式が不正なため保留'
        eid, rid = event['id'], event['receipt']
        digest = signature(event)
        if eid in self.seen:
            return '同一イベントの再配信を無視' if self.seen[eid] == digest else 'イベント内容の不一致を保留'
        if event['action'] == 'receive':
            if rid in self.receipts:
                # A different event must not overwrite completed or pending
                # tasks just because it reuses the same receipt ID.
                return '受付番号の重複・変更を保留'
            ids = [t['id'] for t in event['tasks']]
            if not ids or len(ids) != len(set(ids)):
                return '用件番号の不足・重複を保留'
            tasks = []
            for task in event['tasks']:
                row = {'用件番号':task['id'],'状態':'未対応','担当先':'受付担当','記録':'','回答':''}
                if task['kind'] == 'order':
                    row['担当先'] = '受注担当'
                    if not task.get('owner'):
                        row['記録'] = '本人確認情報がないため照会しない'
                    else:
                        value, error = self.orders.fetch_for_user(task['owner'], task['order'])
                        if error:
                            row['記録'] = error
                        else:
                            row.update({'状態':'回答済み','回答':value['status'],'記録':'架空の注文情報を取得'})
                elif task['kind'] == 'change':
                    row['担当先'], row['記録'] = '請求担当', '宛名変更の受付。変更処理は未実行'
                else:
                    row['記録'] = '未定義の用件。自動完了しない'
                tasks.append(row)
            self.receipts[rid] = tasks
            self.seen[eid] = digest
            return '受付を記録'
        if event['action'] == 'finish':
            target = next((t for t in self.receipts.get(rid,[]) if t['用件番号']==event['task']),None)
            if target is None:
                return '存在しない用件の完了を保留'
            if not event.get('note') or not event['note'].strip():
                return '完了理由がないため保留'
            target['状態'],target['記録'] = '対応済み',event['note']
            self.seen[eid] = digest
            return '担当者の完了入力を記録'
        return '未定義の操作を保留'

    def state(self, rid):
        tasks = self.receipts.get(rid,[])
        return '完了' if tasks and all(t['状態'] in ('回答済み','対応済み') for t in tasks) else '未完了'

    def rows(self):
        return [{'受付番号':rid,**t,'受付全体':self.state(rid)} for rid,tasks in self.receipts.items() for t in tasks]

def run(events):
    orders = FakeOrders()
    ledger = Ledger(orders)
    steps = []
    for index,event in enumerate(events,1):
        outcome = ledger.apply(event)
        steps.append({'step':index,'event':event['id'],'outcome':outcome,'rows':ledger.rows(),'orderCalls':list(orders.calls),'access':list(orders.access)})
    return ledger, orders, steps

if __name__ == '__main__':
    events = json.loads((HERE/'events.json').read_text())
    ledger, orders, steps = run(events)
    report = {'scope':'Offline fake data boundary and receipt state only. No AI, real authentication, network or concurrency.','steps':steps,'finalRows':ledger.rows(),'orderCalls':orders.calls,'access':orders.access}
    (HERE/'observed.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n')
    with (HERE/'receipt-records.csv').open('w',encoding='utf-8-sig',newline='') as f:
        writer=csv.DictWriter(f,fieldnames=['受付番号','用件番号','状態','担当先','記録','回答','受付全体'],lineterminator='\n')
        writer.writeheader();writer.writerows(ledger.rows())
    print(json.dumps({'steps':len(steps),'rows':len(ledger.rows()),'orderCalls':orders.calls,'finalState':{rid:ledger.state(rid) for rid in ledger.receipts}},ensure_ascii=False))
