#!/usr/bin/env python3
"""Search within one 1C object context using configurator-visible terms."""

from __future__ import annotations

import argparse
import base64
import json
import sys
from pathlib import Path
from typing import Any

sys.path.insert(0, str(Path(__file__).resolve().parent))
from get_1c_form_context import attach_structure, select_form  # noqa: E402
from get_1c_object_brief_context import view_forms, view_modules  # noqa: E402
from get_1c_object_metadata import build_object_metadata  # noqa: E402
from resolve_1c_object import load_json, resolve_object  # noqa: E402


def decode_arg(value: str | None, encoded: str | None) -> str | None:
    if encoded:
        return base64.b64decode(encoded).decode("utf-8")
    return value


def norm(value: Any) -> str:
    return "".join(ch for ch in str(value or "").casefold() if ch not in " \t\r\n._-")


def text_fields(value: Any) -> list[str]:
    fields = []
    if isinstance(value, dict):
        for item in value.values():
            fields.extend(text_fields(item))
    elif isinstance(value, list):
        for item in value:
            fields.extend(text_fields(item))
    elif value is not None:
        fields.append(str(value))
    return fields


def matches(query: str, payload: dict[str, Any]) -> bool:
    wanted = norm(query)
    return any(wanted in norm(text) for text in text_fields(payload))


def slim(payload: dict[str, Any], keys: list[str]) -> dict[str, Any]:
    return {key: payload.get(key) for key in keys if payload.get(key) not in (None, [], "")}


def add_match(result: list[dict[str, Any]], *, area: str, score: int, payload: dict[str, Any], evidence: dict[str, Any]) -> None:
    row = {"area": area, "score": score, **payload, "evidence": evidence}
    result.append(row)


def search_metadata(query: str, metadata: dict[str, Any], limit: int) -> list[dict[str, Any]]:
    result = []
    for attr in metadata.get("attributes") or []:
        payload = slim(attr, ["name", "synonym", "type", "types", "origin", "effective_action"])
        if matches(query, payload):
            add_match(result, area="metadata.attribute", score=90, payload=payload, evidence={"uuid": attr.get("uuid")})
    for section in metadata.get("tabular_sections") or []:
        payload = slim(section, ["name", "synonym", "origin"])
        if matches(query, payload):
            add_match(result, area="metadata.tabular_section", score=80, payload=payload, evidence={"uuid": section.get("uuid")})
    return result[:limit]


def search_forms(query: str, forms: list[dict[str, Any]], max_form_items: int, limit: int) -> list[dict[str, Any]]:
    result = []
    for form in attach_structure(select_form(forms, None), max_items=max_form_items):
        form_payload = slim(form, ["name", "synonym", "origin", "effective_action", "form_xml_path", "module_path"])
        if matches(query, form_payload):
            add_match(result, area="form", score=75, payload=form_payload, evidence={"uuid": form.get("uuid")})
        structures = []
        if form.get("structure"):
            structures.append((form, form.get("structure")))
        for overlay in form.get("extension_overlays") or []:
            if overlay.get("structure"):
                structures.append((overlay, overlay.get("structure")))
        for owner, structure in structures:
            origin = owner.get("origin") or {}
            for item in structure.get("items") or []:
                payload = slim(item, ["name", "kind", "title", "data_path", "command_name", "parent", "events"])
                if matches(query, payload):
                    payload["form"] = owner.get("name")
                    payload["origin"] = origin
                    add_match(result, area="form.item", score=70, payload=payload, evidence={"id": item.get("id"), "form_xml_path": owner.get("form_xml_path")})
            for attr in structure.get("attributes") or []:
                payload = slim(attr, ["name", "title", "data_path", "type", "saved_data", "view"])
                if matches(query, payload):
                    payload["form"] = owner.get("name")
                    payload["origin"] = origin
                    add_match(result, area="form.attribute", score=72, payload=payload, evidence={"id": attr.get("id"), "form_xml_path": owner.get("form_xml_path")})
            for command in structure.get("commands") or []:
                payload = slim(command, ["name", "title", "action", "group", "representation"])
                if matches(query, payload):
                    payload["form"] = owner.get("name")
                    payload["origin"] = origin
                    add_match(result, area="form.command", score=74, payload=payload, evidence={"id": command.get("id"), "form_xml_path": owner.get("form_xml_path")})
            for event in structure.get("events") or []:
                payload = slim(event, ["name", "handler"])
                if matches(query, payload):
                    payload["form"] = owner.get("name")
                    payload["origin"] = origin
                    add_match(result, area="form.event", score=76, payload=payload, evidence={"form_xml_path": owner.get("form_xml_path")})
            if len(result) >= limit:
                return result[:limit]
    return result[:limit]


def line_snippet(lines: list[str], index: int, radius: int = 1) -> dict[str, Any]:
    start = max(0, index - radius)
    end = min(len(lines), index + radius + 1)
    return {
        "line": index + 1,
        "text": "".join(lines[start:end]).strip(),
    }


def search_modules(query: str, modules: list[dict[str, Any]], *, search_code: bool, limit: int) -> list[dict[str, Any]]:
    result = []
    flat_modules = []
    for module in modules:
        flat_modules.append(module)
        flat_modules.extend(module.get("extension_overlays") or [])
    for module in flat_modules:
        payload = slim(module, ["name", "kind", "origin", "effective_action", "relative_path", "path", "size"])
        if matches(query, payload):
            add_match(result, area="module", score=65, payload=payload, evidence={"path": module.get("path")})
        if not search_code:
            continue
        path = Path(str(module.get("path") or ""))
        if not path.is_file():
            continue
        try:
            lines = path.read_text(encoding="utf-8-sig").splitlines(keepends=True)
        except UnicodeDecodeError:
            lines = path.read_text(encoding="cp1251", errors="replace").splitlines(keepends=True)
        wanted = norm(query)
        for index, line in enumerate(lines):
            if wanted and wanted in norm(line):
                add_match(
                    result,
                    area="module.code",
                    score=60,
                    payload=payload,
                    evidence={"path": str(path), **line_snippet(lines, index)},
                )
                if len(result) >= limit:
                    return result[:limit]
    return result[:limit]


def build_search(
    index: dict[str, Any],
    *,
    kind: str,
    name: str,
    query: str,
    view: str,
    extension: str | None,
    search_code: bool,
    max_form_items: int,
    limit: int,
) -> dict[str, Any]:
    resolution = resolve_object(index, kind=kind, name=name, limit=100)
    canonical = resolution.get("canonical")
    if not canonical:
        raise SystemExit(f"Object not found: {kind}.{name}")
    metadata = build_object_metadata(index, kind=kind, name=name, view=view, extension=extension, include_storage=False)
    forms, form_counts = view_forms(canonical, resolution, view=view, extension=extension)
    modules, module_counts = view_modules(canonical, resolution, view=view, extension=extension)

    matches_found = []
    remaining = limit
    for group in (
        search_metadata(query, metadata, remaining),
        search_forms(query, forms, max_form_items=max_form_items, limit=remaining),
        search_modules(query, modules, search_code=search_code, limit=remaining),
    ):
        matches_found.extend(group)
        remaining = limit - len(matches_found)
        if remaining <= 0:
            break
    matches_found.sort(key=lambda row: row.get("score", 0), reverse=True)
    return {
        "schema": "onec_object_context_search.v1",
        "view": view,
        "extension": extension if view == "extension" else None,
        "query": {"kind": kind, "name": name, "text": query, "search_code": search_code, "max_form_items": max_form_items, "limit": limit},
        "object": {
            "kind": canonical.get("kind"),
            "name": canonical.get("name"),
            "synonym": canonical.get("synonym"),
            "uuid": canonical.get("guid"),
        },
        "matches": matches_found[:limit],
        "counts": {
            "matches": len(matches_found[:limit]),
            "forms": form_counts,
            "modules": module_counts,
            "metadata_attributes": len(metadata.get("attributes") or []),
            "metadata_tabular_sections": len(metadata.get("tabular_sections") or []),
        },
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Search one 1C object context.")
    parser.add_argument("--index", type=Path, required=True)
    parser.add_argument("--kind")
    parser.add_argument("--name")
    parser.add_argument("--text")
    parser.add_argument("--kind-b64")
    parser.add_argument("--name-b64")
    parser.add_argument("--text-b64")
    parser.add_argument("--view", choices=["effective", "base", "extension"], default="effective")
    parser.add_argument("--extension")
    parser.add_argument("--search-code", action="store_true")
    parser.add_argument("--max-form-items", type=int, default=2000)
    parser.add_argument("--limit", type=int, default=50)
    parser.add_argument("--output", type=Path)
    args = parser.parse_args()

    kind = decode_arg(args.kind, args.kind_b64)
    name = decode_arg(args.name, args.name_b64)
    text = decode_arg(args.text, args.text_b64)
    if not kind or not name or not text:
        raise SystemExit("Use --kind/--name/--text or their --*-b64 variants.")
    result = build_search(
        load_json(args.index),
        kind=kind,
        name=name,
        query=text,
        view=args.view,
        extension=args.extension,
        search_code=args.search_code,
        max_form_items=args.max_form_items,
        limit=args.limit,
    )
    output = json.dumps(result, ensure_ascii=False, indent=2) + "\n"
    if args.output:
        args.output.parent.mkdir(parents=True, exist_ok=True)
        args.output.write_text(output, encoding="utf-8")
        print(json.dumps({"output": str(args.output), "counts": result["counts"], "view": result["view"]}, ensure_ascii=False))
    else:
        print(output)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
