"""Parser for Condensa Code contract messages.

Parses !:fn, !:type, !:wire, !:def, !:api, !:schema, and ?:check
messages into structured ContractSet objects.
"""

from __future__ import annotations

import re

from .types import (
    PropSpec,
    FieldSpec,
    FnContract,
    TypeContract,
    WireContract,
    DefContract,
    ApiContract,
    SchemaContract,
    ContractSet,
)


def parse_contract(text: str) -> ContractSet:
    """Parse one or more Condensa Code lines into a ContractSet.

    Accepts multiline text with one contract per line.
    Lines starting with !:fn, !:type, !:wire, !:def, !:api, !:schema
    are parsed. Other lines are ignored.

    Also handles seq() wrappers — strips them before parsing.
    """
    contracts = ContractSet()

    # Strip seq() wrapper if present
    stripped = text.strip()
    if stripped.startswith("seq(") and stripped.endswith(")"):
        stripped = stripped[4:-1]

    for line in stripped.split("\n"):
        line = line.strip()
        if not line:
            continue
        # Handle semicolon-separated lines (from seq())
        for part in line.split(";"):
            part = part.strip()
            if part:
                _parse_line_into(part, contracts)

    return contracts


def parse_line(line: str) -> FnContract | TypeContract | WireContract | DefContract | ApiContract | SchemaContract | None:
    """Parse a single Condensa Code line. Returns the parsed contract or None."""
    contracts = ContractSet()
    _parse_line_into(line.strip(), contracts)

    # Return the first parsed item
    if contracts.functions:
        return next(iter(contracts.functions.values()))
    if contracts.types:
        return next(iter(contracts.types.values()))
    if contracts.wires:
        return contracts.wires[0]
    if contracts.modules:
        return next(iter(contracts.modules.values()))
    if contracts.apis:
        return contracts.apis[0]
    if contracts.schemas:
        return next(iter(contracts.schemas.values()))
    return None


def _parse_line_into(line: str, contracts: ContractSet) -> None:
    """Parse a single line and add to the ContractSet."""
    if line.startswith("!:fn ") or line.startswith("!?:fn "):
        _parse_fn(line, contracts)
    elif line.startswith("!:type "):
        _parse_type(line, contracts)
    elif line.startswith("!:wire ") or line.startswith("!?:wire "):
        _parse_wire(line, contracts)
    elif line.startswith("!:def "):
        _parse_def(line, contracts)
    elif line.startswith("!:api "):
        _parse_api(line, contracts)
    elif line.startswith("!:schema "):
        _parse_schema(line, contracts)
    elif line.startswith("@:define "):
        _parse_at_define(line, contracts)


# ── Individual parsers ───────────────────────────────────────────────

def _parse_fn(line: str, contracts: ContractSet) -> None:
    """Parse !:fn Name /props:(...) /returns:type /renders:(...)."""
    # Strip prefix
    body = re.sub(r'^![\?]?:fn\s+', '', line)
    parts = body.split()
    if not parts:
        return

    name = parts[0]
    fn = FnContract(name=name)

    # Extract /props:(...)
    props_match = re.search(r'/props:\(([^)]*)\)', line)
    if props_match:
        fn.props = _parse_prop_list(props_match.group(1))

    # Extract /returns:type
    returns_match = re.search(r'/returns:(\S+)', line)
    if returns_match:
        fn.returns = returns_match.group(1)

    # Extract /throws:type
    throws_match = re.search(r'/throws:(\S+)', line)
    if throws_match:
        fn.throws = throws_match.group(1)

    # Extract /renders:(...)
    renders_match = re.search(r'/renders:\(([^)]*)\)', line)
    if renders_match:
        fn.renders = [r.strip() for r in renders_match.group(1).split(",")]

    contracts.functions[name] = fn


def _parse_type(line: str, contracts: ContractSet) -> None:
    """Parse !:type Name { field:type ... }."""
    body = line[len("!:type "):].strip()

    # Extract name and fields
    brace_match = re.match(r'(\w+)\s*\{([^}]*)\}', body)
    if brace_match:
        name = brace_match.group(1)
        fields_str = brace_match.group(2).strip()
        fields = _parse_field_list(fields_str)
        contracts.types[name] = TypeContract(name=name, fields=fields)
    else:
        # No braces — just a name
        name = body.split()[0] if body.split() else body
        contracts.types[name] = TypeContract(name=name)


def _parse_wire(line: str, contracts: ContractSet) -> None:
    """Parse !:wire source.port -> target.port."""
    body = re.sub(r'^![\?]?:wire\s+', '', line)

    # Split on ->
    segments = [s.strip() for s in body.split("->")]
    for i in range(len(segments) - 1):
        contracts.wires.append(WireContract(
            source=segments[i],
            target=segments[i + 1],
        ))


def _parse_def(line: str, contracts: ContractSet) -> None:
    """Parse !:def name /exports:(...) /deps:(...) /pages:(...)."""
    body = line[len("!:def "):].strip()
    parts = body.split()
    if not parts:
        return

    name = parts[0]
    mod = DefContract(name=name)

    exports_match = re.search(r'/exports:\(([^)]*)\)', line)
    if exports_match:
        mod.exports = [e.strip() for e in exports_match.group(1).split(",")]

    deps_match = re.search(r'/deps:\(([^)]*)\)', line)
    if deps_match:
        mod.deps = [d.strip() for d in deps_match.group(1).split(",")]

    pages_match = re.search(r'/pages:\(([^)]*)\)', line)
    if pages_match:
        mod.pages = [p.strip() for p in pages_match.group(1).split(",")]

    types_match = re.search(r'/types:\(([^)]*)\)', line)
    if types_match:
        mod.types = [t.strip() for t in types_match.group(1).split(",")]

    contracts.modules[name] = mod


def _parse_api(line: str, contracts: ContractSet) -> None:
    """Parse !:api METHOD /path /params:(...) /body:Type /returns:Type."""
    body = line[len("!:api "):].strip()
    parts = body.split()
    if len(parts) < 2:
        return

    method = parts[0].upper()
    path = parts[1]
    api = ApiContract(method=method, path=path)

    params_match = re.search(r'/params:\(([^)]*)\)', line)
    if params_match:
        api.params = _parse_prop_list(params_match.group(1))

    body_match = re.search(r'/body:(\S+)', line)
    if body_match:
        api.body = body_match.group(1)

    returns_match = re.search(r'/returns:(\S+)', line)
    if returns_match:
        api.returns = returns_match.group(1)

    status_match = re.search(r'/status:\(([^)]*)\)', line)
    if status_match:
        api.status_codes = [int(s.strip()) for s in status_match.group(1).split(",") if s.strip().isdigit()]

    contracts.apis.append(api)


def _parse_schema(line: str, contracts: ContractSet) -> None:
    """Parse !:schema name { field:type constraints }."""
    body = line[len("!:schema "):].strip()

    brace_match = re.match(r'(\w+)\s*\{([^}]*)\}', body)
    if brace_match:
        name = brace_match.group(1)
        fields_str = brace_match.group(2).strip()
        fields = _parse_field_list(fields_str)
        contracts.schemas[name] = SchemaContract(name=name, fields=fields)
    else:
        name = body.split()[0] if body.split() else body
        contracts.schemas[name] = SchemaContract(name=name)


# ── @:define fallback parser ──────────────────────────────────────────

def _parse_at_define(line: str, contracts: ContractSet) -> None:
    """Parse @:define lines that LLMs sometimes produce instead of !:fn/!:type/!:wire.

    Handles patterns like:
      @:define component:Name props:{...}
      @:define type:Name {...}
      @:define wire source -> target
      @:define module:Name pages:(...)
      @:define api METHOD:/path -> ReturnType
      @:define Name {...}           (treated as type)
      @:define Name(props)          (treated as fn)
      @:define Name props:{...}     (treated as fn)
    """
    body = line[len("@:define "):].strip()
    if not body:
        return

    # @:define wire source -> target
    if body.startswith("wire ") or " -> " in body:
        wire_body = body
        if wire_body.startswith("wire "):
            wire_body = wire_body[5:].strip()
        segments = [s.strip() for s in wire_body.split("->")]
        for i in range(len(segments) - 1):
            contracts.wires.append(WireContract(source=segments[i], target=segments[i + 1]))
        return

    # @:define component:Name props:{...} or @:define component:Name props:(...)
    comp_match = re.match(r'component:(\w+)\s+props:\{([^}]*)\}', body)
    if not comp_match:
        comp_match = re.match(r'component:(\w+)\s+props:\(([^)]*)\)', body)
    if comp_match:
        name = comp_match.group(1)
        props_str = comp_match.group(2)
        props = _parse_prop_list_flexible(props_str)
        contracts.functions[name] = FnContract(name=name, props=props)
        return

    # @:define type:Name {...}
    type_match = re.match(r'type:(\w+)\s*\{([^}]*)\}', body)
    if type_match:
        name = type_match.group(1)
        fields = _parse_field_list_flexible(type_match.group(2))
        contracts.types[name] = TypeContract(name=name, fields=fields)
        return

    # @:define module:Name pages:(...)
    mod_match = re.match(r'module:(\w+)\s+pages:\(([^)]*)\)', body)
    if mod_match:
        name = mod_match.group(1)
        pages = [p.strip() for p in mod_match.group(2).split(",")]
        contracts.modules[name] = DefContract(name=name, pages=pages)
        return

    # @:define api METHOD:/path -> ReturnType
    api_match = re.match(r'api\s+(GET|POST|PUT|DELETE|PATCH):?(/\S+)(?:\s*->\s*(\S+))?', body, re.IGNORECASE)
    if api_match:
        method = api_match.group(1).upper()
        path = api_match.group(2)
        returns = api_match.group(3) or ""
        contracts.apis.append(ApiContract(method=method, path=path, returns=returns))
        return

    # @:define Name {...}  (type)
    brace_match = re.match(r'(\w+)\s*\{([^}]*)\}', body)
    if brace_match:
        name = brace_match.group(1)
        fields = _parse_field_list_flexible(brace_match.group(2))
        contracts.types[name] = TypeContract(name=name, fields=fields)
        return

    # @:define Name(props) or @:define Name props:{...}
    fn_match = re.match(r'(\w+)\s*\(([^)]*)\)', body)
    if fn_match:
        name = fn_match.group(1)
        props = _parse_prop_list_flexible(fn_match.group(2))
        contracts.functions[name] = FnContract(name=name, props=props)
        return

    fn_match2 = re.match(r'(\w+)\s+props:\{([^}]*)\}', body)
    if fn_match2:
        name = fn_match2.group(1)
        props = _parse_prop_list_flexible(fn_match2.group(2))
        contracts.functions[name] = FnContract(name=name, props=props)
        return


def _parse_prop_list_flexible(text: str) -> list[PropSpec]:
    """Parse props from LLM output which may use commas or spaces, various type formats."""
    # Normalize: replace commas with spaces, clean up
    normalized = text.replace(",", " ")
    tokens = _split_respecting_parens(normalized)
    props = []
    for token in tokens:
        token = token.strip()
        if not token:
            continue
        # Handle "name:Type" format
        if ":" in token:
            props.append(PropSpec.parse(token))
        elif token and not token[0].isdigit():
            # Bare name
            props.append(PropSpec(name=token, type="any"))
    return props


def _parse_field_list_flexible(text: str) -> list[FieldSpec]:
    """Parse fields from LLM output which may use commas, various type notations."""
    # Normalize: replace commas with spaces
    normalized = text.replace(",", " ")
    tokens = normalized.split()
    fields = []
    for token in tokens:
        token = token.strip()
        if not token:
            continue
        if ":" in token:
            name, type_str = token.split(":", 1)
            # Normalize LLM type names
            type_map = {
                "number": "n", "num": "n", "int": "n", "integer": "n", "float": "n",
                "string": "s", "str": "s", "text": "s",
                "boolean": "b", "bool": "b",
            }
            required = type_str.endswith("!") or type_str.endswith("*")
            optional = type_str.endswith("?")
            clean_type = type_str.rstrip("!*?")
            clean_type = type_map.get(clean_type, clean_type)
            fields.append(FieldSpec(
                name=name, type=clean_type, required=required, optional=optional,
            ))
    return fields


# ── Helpers ──────────────────────────────────────────────────────────

def _parse_prop_list(text: str) -> list[PropSpec]:
    """Parse a space-separated prop list like 'name:type name2:type2'."""
    props = []
    # Split by spaces but respect parentheses (for fn types)
    tokens = _split_respecting_parens(text)
    for token in tokens:
        token = token.strip()
        if token:
            props.append(PropSpec.parse(token))
    return props


def _parse_field_list(text: str) -> list[FieldSpec]:
    """Parse a space-separated field list like 'id:n pk name:s* email:s unique'."""
    fields = []
    tokens = text.split()
    i = 0
    while i < len(tokens):
        token = tokens[i]
        if ":" in token:
            name, type_str = token.split(":", 1)
            required = type_str.endswith("*")
            optional = type_str.endswith("?")
            # Collect trailing constraints (pk, fk:X, unique, idx)
            constraints = []
            while i + 1 < len(tokens):
                next_token = tokens[i + 1]
                if next_token in ("pk", "unique", "idx") or next_token.startswith("fk:"):
                    i += 1
                    constraints.append(next_token)
                elif ":" not in next_token:
                    break
                else:
                    break
            fields.append(FieldSpec(
                name=name,
                type=type_str.rstrip("*?"),
                required=required,
                optional=optional,
                constraints=constraints,
            ))
        i += 1
    return fields


def _split_respecting_parens(text: str) -> list[str]:
    """Split by spaces but respect parentheses depth."""
    tokens = []
    current = ""
    depth = 0
    for ch in text:
        if ch == "(":
            depth += 1
            current += ch
        elif ch == ")":
            depth -= 1
            current += ch
        elif ch == " " and depth == 0:
            if current:
                tokens.append(current)
                current = ""
        else:
            current += ch
    if current:
        tokens.append(current)
    return tokens
