import re
from dataclasses import dataclass, field


@dataclass
class BlockContract:
    type: str
    max_changed_lines: int = 6
    start_anchor: str = ""
    end_anchor: str = ""
    old_line: str = ""

    def as_dict(self) -> dict:
        return {
            "type": self.type,
            "max_changed_lines": self.max_changed_lines,
            "start_anchor": self.start_anchor,
            "end_anchor": self.end_anchor,
            "old_line": self.old_line,
        }


@dataclass
class FileEditContract:
    path: str
    reason: str
    intent: str = "update"
    max_hunks: int = 1
    max_changed_lines: int = 8
    allowed_blocks: list[BlockContract] = field(default_factory=list)

    def as_dict(self) -> dict:
        return {
            "path": self.path,
            "reason": self.reason,
            "intent": self.intent,
            "max_hunks": self.max_hunks,
            "max_changed_lines": self.max_changed_lines,
            "allowed_blocks": [block.as_dict() for block in self.allowed_blocks],
        }


class ContractParser:
    _supported_block_types = {"append_end", "replace_between", "replace_line_equals"}

    def parse(self, payload: dict, *, request: str, requested_path: str) -> list[dict]:
        files = payload.get("files", []) if isinstance(payload, dict) else []
        parsed: list[FileEditContract] = []
        for item in files if isinstance(files, list) else []:
            contract = self._parse_file_contract(item)
            if contract:
                parsed.append(contract)

        if not parsed:
            fallback = self._fallback_contract(request=request, requested_path=requested_path)
            if fallback:
                parsed.append(fallback)

        return [item.as_dict() for item in parsed]

    def _parse_file_contract(self, item: object) -> FileEditContract | None:
        if not isinstance(item, dict):
            return None
        path = str(item.get("path", "")).replace("\\", "/").strip()
        if not path:
            return None
        reason = str(item.get("reason", "")).strip() or "Requested user adjustment."
        intent = str(item.get("intent", "update")).strip().lower() or "update"
        if intent not in {"update", "create"}:
            intent = "update"
        max_hunks = self._clamp_int(item.get("max_hunks"), default=1, min_value=1, max_value=5)
        max_changed_lines = self._clamp_int(item.get("max_changed_lines"), default=8, min_value=1, max_value=120)
        blocks: list[BlockContract] = []
        raw_blocks = item.get("allowed_blocks", [])
        for raw in raw_blocks if isinstance(raw_blocks, list) else []:
            block = self._parse_block(raw)
            if block:
                blocks.append(block)
        if not blocks:
            return None
        return FileEditContract(
            path=path,
            reason=reason,
            intent=intent,
            max_hunks=max_hunks,
            max_changed_lines=max_changed_lines,
            allowed_blocks=blocks,
        )

    def _parse_block(self, raw: object) -> BlockContract | None:
        if not isinstance(raw, dict):
            return None
        kind = self._normalize_block_type(str(raw.get("type", "")).strip().lower())
        if kind not in self._supported_block_types:
            return None
        max_changed_lines = self._clamp_int(raw.get("max_changed_lines"), default=6, min_value=1, max_value=80)
        block = BlockContract(
            type=kind,
            max_changed_lines=max_changed_lines,
            start_anchor=str(raw.get("start_anchor", "")).strip(),
            end_anchor=str(raw.get("end_anchor", "")).strip(),
            old_line=str(raw.get("old_line", "")).strip(),
        )
        if block.type == "replace_between" and (not block.start_anchor or not block.end_anchor):
            return None
        if block.type == "replace_line_equals" and not block.old_line:
            return None
        return block

    def _fallback_contract(self, *, request: str, requested_path: str) -> FileEditContract | None:
        path = requested_path.strip()
        if not path:
            return None
        low = (request or "").lower()
        if any(marker in low for marker in ("в конец", "в самый конец", "append to end", "append at the end")):
            return FileEditContract(
                path=path,
                reason="Append-only update inferred from user request.",
                intent="update",
                max_hunks=1,
                max_changed_lines=8,
                allowed_blocks=[BlockContract(type="append_end", max_changed_lines=8)],
            )
        quoted = self._extract_quoted_line(request)
        if quoted:
            return FileEditContract(
                path=path,
                reason="Single-line replacement inferred from quoted segment in user request.",
                intent="update",
                max_hunks=1,
                max_changed_lines=4,
                allowed_blocks=[BlockContract(type="replace_line_equals", old_line=quoted, max_changed_lines=4)],
            )
        return None

    def _extract_quoted_line(self, text: str) -> str:
        value = (text or "").strip()
        patterns = [
            r"`([^`]+)`",
            r"\"([^\"]+)\"",
            r"'([^']+)'",
            r"«([^»]+)»",
        ]
        for pattern in patterns:
            match = re.search(pattern, value)
            if not match:
                continue
            candidate = match.group(1).strip()
            if candidate:
                return candidate
        return ""

    def _normalize_block_type(self, value: str) -> str:
        mapping = {
            "append": "append_end",
            "append_eof": "append_end",
            "end_append": "append_end",
            "replace_block": "replace_between",
            "replace_section": "replace_between",
            "replace_range": "replace_between",
            "replace_line": "replace_line_equals",
            "line_equals": "replace_line_equals",
        }
        return mapping.get(value, value)

    def _clamp_int(self, value: object, *, default: int, min_value: int, max_value: int) -> int:
        try:
            numeric = int(value)  # type: ignore[arg-type]
        except Exception:
            numeric = default
        return max(min_value, min(max_value, numeric))
