The requested function should report the maximum nesting depth of balanced parentheses in a string, while rejecting an unbalanced string. Characters other than parentheses are ordinary text and do not contribute to depth. For a string without any parentheses, the maximum depth is zero. I will return an integer for a valid string and raise ValueError for an invalid one. This gives a caller a clear distinction between an empty nesting structure and malformed input rather than assigning both of them the same numeric result.

A stack is one natural way to process brackets. When we encounter an opening parenthesis, we could push its position, and when we encounter a closing parenthesis, we could pop an opening position. The largest stack size would be the answer. However, because there is only one kind of bracket and we do not need to return matching positions, the content of the stack contains more information than this task needs. Its size alone is sufficient. I can replace the stack with an integer called depth and keep a second integer for the largest value depth has reached.

The meaning of depth at a position is the number of opening parentheses in the prefix that have not yet been closed. An opening parenthesis increments it; a closing parenthesis decrements it. Other characters leave it unchanged. This interpretation immediately supplies two validity requirements. First, depth must never become negative, because a negative count would mean that a closing parenthesis has appeared without a matching opening parenthesis to its left. Second, depth must be zero after the entire string is processed, because a positive final count means that some opening parentheses never received a closing partner.

It is important to check both conditions. Testing only the final count would accept the string ")(": the count returns to zero even though the first character cannot be matched correctly. Testing only for negative prefixes would accept "(()", which never has an unmatched closer but ends with an unmatched opener. Together, the prefix condition and the final zero condition characterize balanced strings with one bracket type. The ordinary text between brackets does not affect either property, so it can be ignored during the scan without changing the result.

To compute the maximum, update best immediately after increasing depth for an opening parenthesis. Only an opening parenthesis can create a new record depth. A closing parenthesis reduces depth, and a normal character preserves it, so updating best in those branches would be redundant. Starting both counters at zero handles empty input naturally. On a string such as "a(b(c)d)e", the opening after a brings depth to one and best to one; the next opening brings both to two. Subsequent closings reduce the active depth, but best remains two.

Here is a slightly more involved trace. In "(left)(middle(inner))(right)", the first component reaches depth one and returns to zero. The second component reaches depth two before returning to zero. The third reaches only depth one. We should return two, because the maximum is taken over the whole string, including separate balanced components. Resetting best when depth returns to zero would incorrectly forget earlier components. Only the active depth changes as components close; the maximum is an accumulated statistic of all prefixes visited so far.

The loop invariant can be stated precisely. Before each character is read, depth equals the number of unmatched opening parentheses in the processed prefix, every processed closer has a matching opener, and best is the greatest depth observed among those prefixes. Reading an ordinary character changes none of these facts. Reading an opener increases the unmatched count by one, and updating best records its possible new maximum. Reading a closer is valid only when depth was positive; decrementing then matches that closer to one of the outstanding openers. A negative result identifies invalidity as soon as it becomes unavoidable.

After the loop, the invariant ensures that every closing parenthesis has been matched. If depth is also zero, all opening parentheses have been matched too. Thus the string is balanced, and best is the maximum nesting depth by its definition. If depth is positive, the function raises instead of returning an answer for an incomplete expression. There is no need to store a list of all prefix depths, because the current count and maximum are sufficient statistics for the remaining work.

Error messages can describe whether the problem was an unmatched closer or unmatched opener. Including the position of a closer can help a caller find the malformed part of a long string. An unmatched opener at the end is slightly different: with only a counter, we know how many remain but not their locations. The problem does not require exact opening positions, so a general message is enough. Maintaining a full stack purely to improve that message would be a separate interface choice, and I will avoid it in this small function.

One possible ambiguity is whether parentheses inside quoted text should be ignored. The problem describes a plain string rather than source code, so every literal '(' and ')' counts. Adding quote or escape handling would require a grammar for strings and would change examples such as "'('". The function below intentionally performs the simple character interpretation described at the start. It does not pretend to parse a programming language, comments, or escaped delimiters. This keeps its behavior consistent for any input text and makes the accepted language easy to specify.

There is a related distinction between characters and bytes. Python iterates over string characters, and both parentheses are single characters regardless of the text around them. Accented letters or symbols therefore pass through the ordinary-character branch exactly like letters in the examples. We do not encode the string, count byte offsets, or modify its contents. If a caller supplies something other than a string, ordinary Python iteration might still happen, but supporting arbitrary collections is outside this function's stated contract.

The running time is O(n) for a string of length n, since every character is inspected once and each inspection uses constant work. The extra storage is O(1). These bounds do not depend on nesting depth: even a string with a very deep balanced structure uses the same pair of counters. There is also no recursion, so valid deeply nested input does not run into Python's recursion limit. Early rejection of a bad prefix can reduce the actual work, though the worst-case bound remains linear because a malformed opener may be discovered only at the end.

Before settling on the code, I will check a few small examples mentally. The empty string returns zero. "plain text" also returns zero. "()" reaches one and ends at zero, so it returns one. "((x))" reaches two. "a)b" raises when its closer makes depth negative. "a(b" reaches the final check with depth one and raises for its remaining opener. These examples cover each branch without needing a large wall of near-identical checks. The implementation follows directly from those transitions:

```python
def maximum_parenthesis_depth(text):
    """Return the maximum depth, raising ValueError for unbalanced parentheses."""
    depth = 0
    best = 0
    for position, character in enumerate(text):
        if character == "(":
            depth += 1
            best = max(best, depth)
        elif character == ")":
            depth -= 1
            if depth < 0:
                raise ValueError(f"unmatched closing parenthesis at {position}")
    if depth != 0:
        raise ValueError("unmatched opening parenthesis")
    return best
```
