from __future__ import annotations

import argparse
import configparser
import csv
import json
import re
import shutil
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo


DEFAULT_LOG_SCOPE = (
    "ocid1.compartment.oc1..aaaaaaaau5raupdqclblygmdikwcqt2mglgrfkgdmca6kleven5maaqau22q/"
    "ocid1.loggroup.oc1.sa-saopaulo-1.amaaaaaaaehl73aaude3eznx7pbzbilwwzay5ou2mcccr5aj5tz6rlbyamua"
)
DEFAULT_REGION = "sa-saopaulo-1"
TIA_AGENT_SUBJECT = "tim-ai-atend-agnt-integ-tia-app-agent/0.log"
TIA_BRIDGE_SUBJECT = "tim-ai-atend-agnt-integ-tia-app-bridge/0.log"
CONTAS_NAMESPACE = "agnt-ai-atendimento-contas"

VAD_STT_PATTERNS = [
    "step=vad_activity",
    "step=vad_speech_start",
    "step=vad_speech_end",
    "step=vad_user_pause",
    "step=vad_interrupt_check",
    "step=stt_audio_upload",
    "step=stt_final",
    "USER_STATE",
    "interrupt",
    "tts_start",
    "tts_done",
    "BOT_SAY",
    "agent_input_snapshot",
]

IMPORTANT_EVENTS = {
    "vad_speech_start",
    "vad_speech_end",
    "vad_user_pause",
    "vad_interrupt_check",
    "stt_audio_upload_enqueued",
    "stt_audio_upload_done",
    "stt_final",
    "USER_STATE",
    "tts_start",
    "tts_done",
    "BOT_SAY_HANDLE",
    "BOT_SAY_RESULT",
    "agent_input_snapshot",
}

LOG_LINE_RE = re.compile(r"^(?P<log_time>\S+)\s+(?P<stream>stdout|stderr)\s+(?P<flag>[FP])\s(?P<body>.*)$")
KV_RE = re.compile(r"(?P<key>[a-zA-Z_][a-zA-Z0-9_]*)=(?P<value>[^|]+)")


def clean(value: str | None) -> str:
    return (value or "").strip()


def quote(value: str) -> str:
    return value.replace("\\", "\\\\").replace("'", "\\'")


def contains_wildcard(field: str, value: str) -> str:
    return f"{field} = '*{quote(value)}*'"


def equals_value(field: str, value: str) -> str:
    return f"{field} = '{quote(value)}'"


def starts_with_value(field: str, value: str) -> str:
    return f"{field} = '{quote(value)}*'"


def contains_any(field: str, values: list[str]) -> str:
    unique_values: list[str] = []
    for value in values:
        value = clean(value)
        if value and value not in unique_values:
            unique_values.append(value)
    return "(" + " or ".join(contains_wildcard(field, value) for value in unique_values) + ")"


def _filter_attr(item: Any, name: str, default: str = "") -> str:
    if isinstance(item, dict):
        return clean(str(item.get(name, default) or ""))
    return clean(str(getattr(item, name, default) or ""))


def _simple_filter_values(field_name: str, value: str) -> tuple[str, list[str]]:
    query_field = "data.message"
    if field_name in {"subject", "container"}:
        query_field = "subject"

    values = [value]
    if field_name == "event" and value and "=" not in value:
        values.append(f"step={value}")
    if field_name in {"session_id", "message_id"}:
        compact = hyphenless(value)
        if compact:
            values.append(compact)
    return query_field, values


def simple_filter_expression(item: Any) -> str:
    field_name = _filter_attr(item, "field", "message")
    operator = _filter_attr(item, "operator", "contains")
    value = _filter_attr(item, "value")
    if not value:
        return ""

    query_field, values = _simple_filter_values(field_name, value)
    if operator == "not_contains":
        return "not " + contains_any(query_field, values)
    if operator == "equals":
        return "(" + " or ".join(equals_value(query_field, item_value) for item_value in values) + ")"
    if operator == "starts_with":
        return "(" + " or ".join(starts_with_value(query_field, item_value) for item_value in values) + ")"
    return contains_any(query_field, values)


def simple_filters_group(filters: list[Any]) -> str:
    parts: list[str] = []
    for item in filters:
        expression = simple_filter_expression(item)
        if not expression:
            continue
        connector = _filter_attr(item, "connector", "and").lower()
        connector = "or" if connector == "or" else "and"
        if not parts:
            parts.append(expression)
        else:
            parts.append(f"{connector} {expression}")
    if not parts:
        return ""
    return "(" + " ".join(parts) + ")"


def hyphenless(value: str) -> str | None:
    clean_value = clean(value)
    if re.match(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", clean_value):
        return clean_value.replace("-", "")
    return None


def normalize_time(value: str, time_zone: str) -> str:
    raw = clean(value)
    if not raw:
        raise ValueError("Horario vazio.")
    raw = raw.replace(" ", "T")
    if raw.endswith("Z"):
        parsed = datetime.fromisoformat(raw[:-1] + "+00:00")
    else:
        parsed = datetime.fromisoformat(raw)
    if parsed.tzinfo is None:
        parsed = parsed.replace(tzinfo=ZoneInfo(time_zone))
    return parsed.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")


def windows_path_to_wsl(value: str) -> str:
    match = re.match(r"^([a-zA-Z]):[\\/](.*)$", value.strip())
    if not match:
        return value
    drive = match.group(1).lower()
    rest = match.group(2).replace("\\", "/")
    return f"/mnt/{drive}/{rest}"


def pem_fingerprint(path: Path) -> str | None:
    try:
        from cryptography.hazmat.primitives import serialization
        from cryptography.hazmat.primitives.serialization import load_pem_private_key

        key = load_pem_private_key(path.read_bytes(), password=None)
        der = key.public_key().public_bytes(
            serialization.Encoding.DER,
            serialization.PublicFormat.SubjectPublicKeyInfo,
        )
        return ":".join(f"{byte:02x}" for byte in __import__("hashlib").md5(der).digest())
    except Exception:
        return None


def find_matching_key_file(config_file: Path, key_file: str, fingerprint: str) -> str | None:
    expected = clean(fingerprint).lower()
    if not expected:
        return None

    current = Path(windows_path_to_wsl(key_file)).expanduser()
    if current.exists() and pem_fingerprint(current) == expected:
        return str(current)

    search_dirs = []
    for candidate_dir in (current.parent, config_file.parent):
        if candidate_dir and candidate_dir.exists() and candidate_dir not in search_dirs:
            search_dirs.append(candidate_dir)

    matches: list[Path] = []
    for directory in search_dirs:
        for candidate in directory.glob("*.pem"):
            if pem_fingerprint(candidate) == expected:
                matches.append(candidate)

    unique_matches = sorted({match.resolve(): match for match in matches}.values())
    if len(unique_matches) == 1:
        return str(unique_matches[0])
    return None


def config_profile_value(config: configparser.RawConfigParser, profile: str | None, key: str) -> str:
    profile_name = profile or "DEFAULT"
    if profile_name == "DEFAULT":
        return config.defaults().get(key, "")
    return config.get(profile_name, key, fallback="")


def prepare_cli_config(config_file: str | None, profile: str | None) -> str | None:
    if not config_file or sys.platform.startswith("win"):
        return config_file

    source = Path(config_file).expanduser()
    if not source.exists():
        return config_file

    original = source.read_text(encoding="utf-8")

    def replace_path(match: re.Match[str]) -> str:
        key = match.group("key")
        value = match.group("value").strip()
        converted = windows_path_to_wsl(value)
        return f"{key}{converted}"

    converted = re.sub(
        r"^(?P<key>\s*(?:key_file|security_token_file)\s*=\s*)(?P<value>[a-zA-Z]:[^\r\n]+)$",
        replace_path,
        original,
        flags=re.MULTILINE,
    )
    parser = configparser.RawConfigParser()
    parser.read_string(converted)
    key_file = config_profile_value(parser, profile, "key_file")
    fingerprint = config_profile_value(parser, profile, "fingerprint")
    matching_key_file = find_matching_key_file(source, key_file, fingerprint)
    if matching_key_file and matching_key_file != key_file:
        converted = re.sub(
            r"^(?P<key>\s*key_file\s*=\s*)(?P<value>[^\r\n]+)$",
            lambda match: f"{match.group('key')}{matching_key_file}",
            converted,
            count=1,
            flags=re.MULTILINE,
        )
    if converted == original:
        return config_file

    handle = tempfile.NamedTemporaryFile("w", encoding="utf-8", prefix="oci_config_wsl_", delete=False)
    with handle:
        handle.write(converted)
    return handle.name


def build_search_query(args: argparse.Namespace) -> str:
    subjects = []
    app_scope = getattr(args, "app_scope", "tia")
    if app_scope in {"tia", "all"}:
        if args.include_agent:
            subjects.append(contains_wildcard("subject", TIA_AGENT_SUBJECT))
        if args.include_bridge:
            subjects.append(contains_wildcard("subject", TIA_BRIDGE_SUBJECT))
    if app_scope in {"contas", "all"}:
        subjects.append(contains_wildcard("logContent", CONTAS_NAMESPACE))
    if not subjects:
        raise ValueError("Selecione pelo menos uma aplicacao ou componente para consultar.")

    where_parts = ["(" + " or ".join(subjects) + ")"]
    if not args.show_health:
        where_parts.append("not (" + contains_wildcard("data.message", "GET /health") + ")")

    if args.event_preset == "vad-stt":
        where_parts.append(contains_any("data.message", VAD_STT_PATTERNS))

    id_filter_parts: list[str] = []
    for arg_name, field_name in (
        ("session_id", "data.message"),
        ("message_id", "data.message"),
    ):
        value = clean(getattr(args, arg_name))
        if not value:
            continue
        values = [value]
        compact = hyphenless(value)
        if compact:
            values.append(compact)
        id_filter_parts.append(contains_any(field_name, values))

    for arg_name in ("room", "protocol", "call_id_ged"):
        value = clean(getattr(args, arg_name))
        if value:
            id_filter_parts.append(contains_wildcard("data.message", value))

    if id_filter_parts:
        id_filter = "(" + " and ".join(id_filter_parts) + ")"
        if args.event_preset == "vad-stt" and not args.strict_id_filter:
            raw_vad_filter = contains_any(
                "data.message",
                [
                    "step=vad_activity",
                    "step=vad_speech_start",
                    "step=vad_speech_end",
                    "step=vad_user_pause",
                    "step=vad_interrupt_check",
                    "USER_STATE",
                ],
            )
            where_parts.append(f"({id_filter} or {raw_vad_filter})")
        else:
            where_parts.append(id_filter)

    for text in args.text or []:
        if clean(text):
            where_parts.append(contains_wildcard("data.message", text))

    simple_group = simple_filters_group(list(getattr(args, "simple_filters", []) or []))
    if simple_group:
        where_parts.append(simple_group)

    lines = [f'search "{clean(args.log_scope)}"']
    lines.extend(f"| where {part}" for part in where_parts)
    lines.append("| select datetime, subject, data.message as message")
    lines.append(f"| sort by datetime {args.sort}")
    return "\n".join(lines)


def next_page_token(payload: dict[str, Any]) -> str | None:
    for key in ("opc-next-page", "opcNextPage", "nextPage"):
        value = payload.get(key)
        if value:
            return str(value)
    headers = payload.get("headers")
    if isinstance(headers, dict):
        for key in ("opc-next-page", "opcNextPage", "nextPage"):
            value = headers.get(key)
            if value:
                return str(value)
    data = payload.get("data")
    if isinstance(data, dict):
        for key in ("opc-next-page", "opcNextPage", "nextPage"):
            value = data.get(key)
            if value:
                return str(value)
    return None


def extract_results(payload: dict[str, Any]) -> list[Any]:
    data = payload.get("data", payload)
    if isinstance(data, dict):
        results = data.get("results")
        if isinstance(results, list):
            return results
    if isinstance(data, list):
        return data
    return []


def extract_summary(payload: dict[str, Any]) -> dict[str, Any]:
    data = payload.get("data", payload)
    if isinstance(data, dict) and isinstance(data.get("summary"), dict):
        return data["summary"]
    return {}


def run_oci_page(args: argparse.Namespace, query: str, time_start: str, time_end: str, page: str | None) -> dict[str, Any]:
    python_dir = Path(sys.executable).parent
    venv_dir = Path(sys.prefix) / ("Scripts" if sys.platform.startswith("win") else "bin")
    oci_candidates = [
        shutil.which("oci"),
        shutil.which("oci.exe"),
        str(python_dir / "oci"),
        str(python_dir / "oci.exe"),
        str(python_dir / "oci.cmd"),
        str(venv_dir / "oci"),
        str(venv_dir / "oci.exe"),
        str(venv_dir / "oci.cmd"),
    ]
    oci_bin = args.oci_bin or next((candidate for candidate in oci_candidates if candidate and Path(candidate).exists()), None)
    if not oci_bin:
        raise RuntimeError(
            "Nao encontrei o binario 'oci'. Instale a OCI CLI, instale 'oci-cli' neste venv, "
            "ou passe --oci-bin com o caminho do executavel."
        )

    command = [
        oci_bin,
        "logging-search",
        "search-logs",
        "--region",
        args.region,
        "--time-start",
        time_start,
        "--time-end",
        time_end,
        "--search-query",
        query,
        "--limit",
        str(args.limit),
        "--output",
        "json",
    ]
    if args.profile:
        command.extend(["--profile", args.profile])
    if args.config_file:
        command.extend(["--config-file", args.config_file])
    if page:
        command.extend(["--page", page])

    env = {
        **dict(__import__("os").environ),
        "SUPPRESS_LABEL_WARNING": "True",
        "OCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING": "True",
    }
    completed = subprocess.run(command, check=False, capture_output=True, text=True, encoding="utf-8", env=env)
    if completed.returncode != 0:
        raise RuntimeError(
            "OCI CLI falhou com exit code "
            f"{completed.returncode}.\nSTDERR:\n{completed.stderr.strip()}\nSTDOUT:\n{completed.stdout.strip()}"
        )
    try:
        return json.loads(completed.stdout)
    except json.JSONDecodeError as exc:
        raise RuntimeError(f"OCI CLI nao retornou JSON valido:\n{completed.stdout[:2000]}") from exc


def result_data(result: Any) -> dict[str, Any]:
    if not isinstance(result, dict):
        return {}
    data = result.get("data") or result
    if not isinstance(data, dict):
        return {}
    log_content = data.get("logContent") if isinstance(data.get("logContent"), dict) else {}
    message = data.get("message") or data.get("data.message") or ""
    if not message and isinstance(log_content.get("data"), dict):
        message = (log_content.get("data") or {}).get("message", "")
    return {
        "datetime": data.get("datetime") or log_content.get("time") or "",
        "subject": data.get("subject") or log_content.get("subject") or "",
        "message": message,
    }


def field(body: str, key: str) -> str | None:
    match = re.search(rf"(?:^|\|)\s*{re.escape(key)}=([^|]+)", body)
    if match:
        return match.group(1).strip()
    return None


def parse_message(message: str) -> dict[str, Any]:
    parsed: dict[str, Any] = {
        "log_time": "",
        "body": message or "",
        "event": "",
        "fields": {},
    }
    match = LOG_LINE_RE.match(message or "")
    if match:
        parsed.update(match.groupdict())
    body = str(parsed.get("body") or "")
    fields = {item.group("key"): item.group("value").strip() for item in KV_RE.finditer(body)}
    parsed["fields"] = fields

    event = fields.get("step")
    if not event and body.startswith("{"):
        try:
            event = json.loads(body).get("tipo_evento") or "json"
        except json.JSONDecodeError:
            event = "json"
    if not event:
        event = body.split("|", 1)[0].strip()
    parsed["event"] = event[:100]
    return parsed


def normalize_row(result: Any) -> dict[str, Any]:
    data = result_data(result)
    parsed = parse_message(str(data.get("message") or ""))
    return {
        "datetime": str(data.get("datetime") or ""),
        "subject": str(data.get("subject") or ""),
        "log_time": parsed["log_time"],
        "event": parsed["event"],
        "body": parsed["body"],
        "fields": parsed["fields"],
        "message": data.get("message") or "",
    }


def compact_row(row: dict[str, Any]) -> str:
    fields = row.get("fields") or {}
    details = []
    for key in (
        "old",
        "new",
        "message_id",
        "duration_ms",
        "original_audio_ms",
        "sent_audio_ms",
        "probability",
        "speaking",
        "speech_duration_ms",
        "silence_ms",
        "raw_speech_ms",
        "raw_silence_ms",
        "eligible",
        "text",
        "reason",
        "stage",
    ):
        if key in fields:
            details.append(f"{key}={fields[key]}")
    if not details and row["body"].startswith("{"):
        try:
            payload = json.loads(row["body"])
            for key in ("tipo_evento", "message_id", "callid", "session_id", "mensagem"):
                if payload.get(key):
                    details.append(f"{key}={payload[key]}")
        except json.JSONDecodeError:
            pass
    if not details:
        details.append(row["body"][:220])
    return " | ".join(details)


def print_summary(rows: list[dict[str, Any]]) -> None:
    print(f"linhas={len(rows)}")
    if rows:
        print(f"primeira={rows[0].get('log_time') or rows[0].get('datetime')}")
        print(f"ultima={rows[-1].get('log_time') or rows[-1].get('datetime')}")

    vad_rows = [row for row in rows if row.get("event") == "vad_activity"]
    if vad_rows:
        def probability(row: dict[str, Any]) -> float:
            try:
                return float((row.get("fields") or {}).get("probability") or 0)
            except ValueError:
                return 0.0

        max_row = max(vad_rows, key=probability)
        print("")
        print("vad_activity")
        print(f"count={len(vad_rows)}")
        print(f"max_prob={probability(max_row):.3f} at={max_row.get('log_time') or max_row.get('datetime')}")
        for threshold in (0.30, 0.20, 0.10):
            print(f"above_{threshold:.2f}={sum(1 for row in vad_rows if probability(row) >= threshold)}")
        print(f"speaking_true={sum(1 for row in vad_rows if (row.get('fields') or {}).get('speaking') == 'True')}")

    print("")
    print("timeline")
    for row in rows:
        event = str(row.get("event") or "")
        if event in IMPORTANT_EVENTS or "interrupt" in event:
            print(f"{row.get('log_time') or row.get('datetime')} | {event} | {compact_row(row)}")


def write_json(path: Path, payload: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")


def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=["datetime", "log_time", "event", "subject", "body"])
        writer.writeheader()
        for row in rows:
            writer.writerow({key: row.get(key, "") for key in writer.fieldnames or []})


def parse_args(argv: list[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Busca logs da OCI via OCI CLI e resume eventos VAD/STT.")
    parser.add_argument("--time-start", "--start", required=True, help="Inicio da janela. Use UTC ou informe --timezone.")
    parser.add_argument("--time-end", "--end", required=True, help="Fim da janela. Use UTC ou informe --timezone.")
    parser.add_argument("--timezone", default="UTC", help="Timezone usado quando os horarios vierem sem offset. Ex: America/Sao_Paulo.")
    parser.add_argument("--region", default=DEFAULT_REGION)
    parser.add_argument("--log-scope", default=DEFAULT_LOG_SCOPE)
    parser.add_argument("--profile", default=None)
    parser.add_argument("--config-file", default=None)
    parser.add_argument("--oci-bin", default=None)
    parser.add_argument("--limit", type=int, default=1000)
    parser.add_argument("--max-pages", type=int, default=20)
    parser.add_argument("--max-results", type=int, default=10000)
    parser.add_argument("--sort", choices=["asc", "desc"], default="asc")
    parser.add_argument("--include-agent", action=argparse.BooleanOptionalAction, default=True)
    parser.add_argument("--include-bridge", action=argparse.BooleanOptionalAction, default=False)
    parser.add_argument("--show-health", action="store_true")
    parser.add_argument("--event-preset", choices=["vad-stt", "all"], default="vad-stt")
    parser.add_argument(
        "--strict-id-filter",
        action="store_true",
        help="Aplica session/call/message id de forma estrita. Por padrao, VAD cru tambem entra na janela.",
    )
    parser.add_argument("--session-id")
    parser.add_argument("--message-id")
    parser.add_argument("--room")
    parser.add_argument("--protocol")
    parser.add_argument("--call-id-ged")
    parser.add_argument("--text", action="append", help="Filtro livre em data.message. Pode repetir.")
    parser.add_argument("--output", default=".tmp/oci_logs_cli_search.json")
    parser.add_argument("--csv", dest="csv_path", default=None)
    parser.add_argument("--dry-run", action="store_true")
    parser.add_argument("--no-summary", action="store_true")
    return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
    args = parse_args(argv or sys.argv[1:])
    try:
        time_start = normalize_time(args.time_start, args.timezone)
        time_end = normalize_time(args.time_end, args.timezone)
        query = build_search_query(args)
        args.config_file = prepare_cli_config(args.config_file, args.profile)
    except Exception as exc:
        print(f"Erro nos argumentos: {exc}", file=sys.stderr)
        return 2

    if args.dry_run:
        print("time_start=" + time_start)
        print("time_end=" + time_end)
        print(query)
        return 0

    all_results: list[Any] = []
    summaries: list[dict[str, Any]] = []
    page = None
    try:
        for page_number in range(1, args.max_pages + 1):
            payload = run_oci_page(args, query, time_start, time_end, page)
            results = extract_results(payload)
            summaries.append(extract_summary(payload))
            all_results.extend(results)
            print(f"pagina={page_number} resultados={len(results)} total={len(all_results)}")
            page = next_page_token(payload)
            if not page or len(all_results) >= args.max_results:
                break
    except Exception as exc:
        print(str(exc), file=sys.stderr)
        return 1

    rows = [normalize_row(result) for result in all_results]
    output_payload = {
        "query": query,
        "time_start": time_start,
        "time_end": time_end,
        "result_count": len(rows),
        "summaries": summaries,
        "rows": rows,
    }
    output_path = Path(args.output)
    write_json(output_path, output_payload)
    print(f"json={output_path}")
    if args.csv_path:
        csv_path = Path(args.csv_path)
        write_csv(csv_path, rows)
        print(f"csv={csv_path}")
    if not args.no_summary:
        print("")
        print_summary(rows)
    return 0


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