"""承認処理の最小例 v2 — 修正後（改訂版 rev1）

記事B（SAMPLE-B）で、v1 の不備を要件（REQ-003〜007）に照らして直したバージョン。

修正点（初版）:
  1. 操作者が「割り当てられた承認者」であることを確認する（REQ-003）
  2. 依頼者本人による承認（自己承認）を拒否する（REQ-003）
  3. 対象の状態が「提出済み」である場合だけ承認する（REQ-003 / REQ-006）
  4. 状態変更と履歴追加を一つのトランザクション（まとめて確定・まとめて取消する処理単位）に入れる（REQ-007）
  5. 保存先での更新に状態条件を付け、更新件数で「操作時にも状態が有効だった」ことを確かめる
  6. 差戻しには理由を必須にする（REQ-004）

追加の修正点（改訂版 rev1、GPTレビュー R4 対応）:
  7. 操作者の役割が「承認者」であることも確認する。役割（その人が承認者か）と割当（その依頼の承認者か）は別の条件。
  8. 接続の設定を処理の前に確認する。自動確定モード（isolation_level が None、または autocommit が True）や、
     未確定の処理が残っている接続では、何も変更せずに拒否する。この処理は「with conn:」に確定・取消を任せる
     前提で書かれており、その前提が崩れる設定では取消を保証できないため。

依然として、本人確認（認証）は固定値の代用であり、同時操作や実際の画面・保存先との接続は
この最小実験では確かめていない。
"""
import sqlite3
from datetime import datetime, timezone

from common import ApprovalResult

clock = None  # テストから固定時刻を注入するための差込み口（None なら現在時刻）


def _now() -> str:
    if clock is not None:
        return clock()
    return datetime.now(timezone.utc).isoformat(timespec="seconds")


def _check_connection(conn):
    """この処理が前提にする接続設定かを確かめる。違えば変更前に拒否する。"""
    if getattr(conn, "autocommit", False) is True:          # Python 3.12 以降の autocommit 属性
        return ApprovalResult(ok=False, reason="接続が自動確定モード（autocommit=True）のため処理しない")
    if conn.isolation_level is None:                         # 旧来の自動確定モード
        return ApprovalResult(ok=False, reason="接続が自動確定モード（isolation_level=None）のため処理しない")
    if conn.in_transaction:
        return ApprovalResult(ok=False, reason="未確定の処理が残っている接続のため処理しない")
    return None


def _check_approver(conn, request_id, actor_id):
    row = conn.execute(
        "SELECT state, requester_id, approver_id FROM requests WHERE id = ?", (request_id,)
    ).fetchone()
    if row is None:
        return None, ApprovalResult(ok=False, reason="依頼が存在しない")
    state, requester_id, approver_id = row
    role_row = conn.execute("SELECT role FROM users WHERE id = ?", (actor_id,)).fetchone()
    if role_row is None or role_row[0] != "承認者":
        return None, ApprovalResult(ok=False, reason="操作者の役割が承認者ではない")
    if actor_id != approver_id:
        return None, ApprovalResult(ok=False, reason="割り当てられた承認者ではない")
    if actor_id == requester_id:
        return None, ApprovalResult(ok=False, reason="自己承認は禁止")
    if state != "提出済み":
        return None, ApprovalResult(ok=False, reason=f"対象外の状態（現在：{state}）")
    return row, None


def approve(conn: sqlite3.Connection, request_id: str, actor_id: str, _fault=None) -> ApprovalResult:
    """依頼を承認する（修正後）。

    _fault: テスト専用。状態更新の後、履歴追加の前に呼ばれる。
            同じトランザクション内なので、失敗すると状態更新も取り消される（対応する接続設定のとき）。
    """
    err = _check_connection(conn)
    if err:
        return err
    row, err = _check_approver(conn, request_id, actor_id)
    if err:
        return err

    try:
        with conn:  # 中で例外が出れば取消（rollback）、出なければ確定（commit）。接続は閉じない
            cur = conn.execute(
                "UPDATE requests SET state = '承認済み' WHERE id = ? AND state = '提出済み'",
                (request_id,),
            )
            if cur.rowcount != 1:
                raise RuntimeError("操作時点で対象の状態が有効でなかった")
            if _fault is not None:
                _fault()
            conn.execute(
                "INSERT INTO history (request_id, action, actor_id, at) VALUES (?, '承認', ?, ?)",
                (request_id, actor_id, _now()),
            )
    except Exception as e:  # noqa: BLE001 - 教材のため広く受けて理由を返す
        rolled_back = not conn.in_transaction
        return ApprovalResult(ok=False, reason=f"処理を{'取り消した' if rolled_back else '取り消せなかった'}：{e}")
    return ApprovalResult(ok=True, reason="承認しました")


def send_back(conn: sqlite3.Connection, request_id: str, actor_id: str, reason: str) -> ApprovalResult:
    """依頼を差し戻す（修正後）。理由が空なら拒否する（REQ-004）。"""
    err = _check_connection(conn)
    if err:
        return err
    if not reason or not reason.strip():
        return ApprovalResult(ok=False, reason="差戻しには理由が必要")
    row, err = _check_approver(conn, request_id, actor_id)
    if err:
        return err
    try:
        with conn:
            cur = conn.execute(
                "UPDATE requests SET state = '差戻し' WHERE id = ? AND state = '提出済み'", (request_id,)
            )
            if cur.rowcount != 1:
                raise RuntimeError("操作時点で対象の状態が有効でなかった")
            conn.execute(
                "INSERT INTO history (request_id, action, actor_id, at, note) VALUES (?, '差戻し', ?, ?, ?)",
                (request_id, actor_id, _now(), reason),
            )
    except Exception as e:  # noqa: BLE001
        rolled_back = not conn.in_transaction
        return ApprovalResult(ok=False, reason=f"処理を{'取り消した' if rolled_back else '取り消せなかった'}：{e}")
    return ApprovalResult(ok=True, reason="差し戻しました")
