from __future__ import annotations

import argparse
import json
import re
import os
import shutil
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Literal

from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field

from app.tools.oci_logs_cli_search import (
    DEFAULT_LOG_SCOPE,
    DEFAULT_REGION,
    CONTAS_NAMESPACE,
    TIA_AGENT_SUBJECT,
    TIA_BRIDGE_SUBJECT,
    build_search_query as build_cli_search_query,
    extract_results,
    extract_summary,
    next_page_token,
    normalize_row,
    prepare_cli_config,
    run_oci_page,
    windows_path_to_wsl,
)


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>[^|]+)")
UUID_RE = re.compile(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}$")


class CliAuth(BaseModel):
    mode: Literal["cli"] = "cli"
    config_file: str = "~/.oci/config"
    profile: str = "DEFAULT"
    region: str = DEFAULT_REGION
    oci_bin: str | None = None


class SimpleFilter(BaseModel):
    connector: Literal["and", "or"] = "and"
    field: Literal[
        "message",
        "subject",
        "container",
        "event",
        "session_id",
        "message_id",
        "call_id_ged",
        "room",
        "protocol",
    ] = "message"
    operator: Literal["contains", "not_contains", "equals", "starts_with"] = "contains"
    value: str = ""


class QueryPreviewRequest(BaseModel):
    log_scope: str = DEFAULT_LOG_SCOPE
    app_scope: Literal["tia", "contas", "all"] = "tia"
    sort: Literal["asc", "desc"] = "asc"
    include_agent: bool = True
    include_bridge: bool = True
    hide_health: bool = True
    event_preset: Literal["all", "vad-stt"] = "all"
    strict_id_filter: bool = False
    session_id: str | None = None
    message_id: str | None = None
    room: str | None = None
    protocol: str | None = None
    call_id_ged: str | None = None
    text: str | None = None
    simple_filters: list[SimpleFilter] = Field(default_factory=list)


class LogSearchRequest(QueryPreviewRequest):
    auth: CliAuth = Field(default_factory=CliAuth)
    time_start: str
    time_end: str
    limit: int = Field(default=500, ge=1, le=1000)
    page: str | None = None
    auto_page: bool = True
    max_pages: int = Field(default=20, ge=1, le=50)


class CodexAnalyzeRequest(BaseModel):
    prompt: str = Field(min_length=1, max_length=2_000_000)
    codex_bin: str | None = None
    model: str | None = None
    timeout_seconds: int = Field(default=180, ge=30, le=900)


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


def _api_error(title: str, message: str, hint: str | None = None, raw: str | None = None) -> dict[str, str | None]:
    return {
        "title": title,
        "message": message,
        "hint": hint,
        "raw": raw,
    }


def _parse_datetime(value: str, field_name: str) -> datetime:
    raw = value.strip()
    if not raw:
        raise HTTPException(status_code=400, detail=f"{field_name} vazio.")
    if raw.endswith("Z"):
        raw = raw[:-1] + "+00:00"
    try:
        parsed = datetime.fromisoformat(raw)
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=f"{field_name} invalido: {value}") from exc
    if parsed.tzinfo is None:
        parsed = parsed.replace(tzinfo=timezone.utc)
    return parsed.astimezone(timezone.utc)


def _rfc3339(value: datetime) -> str:
    return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")


def _container_from_subject(subject: str) -> str:
    match = re.search(r"/([^/]+)/0\.log$", subject or "")
    return match.group(1) if match else ""


def _hyphenless(value: str) -> str | None:
    clean = value.strip()
    if UUID_RE.match(clean):
        return clean.replace("-", "")
    return None


def _query_args(filters: QueryPreviewRequest, auth: CliAuth | None = None, *, limit: int = 500) -> argparse.Namespace:
    text = _clean(filters.text)
    return argparse.Namespace(
        log_scope=_clean(filters.log_scope) or DEFAULT_LOG_SCOPE,
        app_scope=filters.app_scope,
        sort=filters.sort,
        include_agent=filters.include_agent,
        include_bridge=filters.include_bridge,
        show_health=not filters.hide_health,
        event_preset=filters.event_preset,
        strict_id_filter=filters.strict_id_filter,
        session_id=_clean(filters.session_id) or None,
        message_id=_clean(filters.message_id) or None,
        room=_clean(filters.room) or None,
        protocol=_clean(filters.protocol) or None,
        call_id_ged=_clean(filters.call_id_ged) or None,
        text=[text] if text else [],
        simple_filters=[item.model_dump() for item in filters.simple_filters],
        region=(auth.region if auth else DEFAULT_REGION),
        profile=(auth.profile if auth else None),
        config_file=(auth.config_file if auth else None),
        oci_bin=(auth.oci_bin if auth else None),
        limit=limit,
    )


def build_search_query(filters: QueryPreviewRequest) -> str:
    return build_cli_search_query(_query_args(filters))


def _ids_from_body(body: str) -> dict[str, str]:
    ids: dict[str, str] = {}
    for item in KV_RE.finditer(body):
        key = item.group("key")
        value = item.group("value").strip().strip('"').strip("'")
        if key in {
            "session_id",
            "message_id",
            "room",
            "protocol",
            "call_id_ged",
            "phone_number",
            "bridge",
            "reason",
            "ws_close_reason",
            "ws_client_disconnected",
        }:
            ids[key] = value

    if body.startswith("{"):
        try:
            payload = json.loads(body)
        except json.JSONDecodeError:
            payload = {}
        for key, payload_key in (
            ("session_id", "session_id"),
            ("message_id", "message_id"),
            ("call_id_ged", "callid"),
        ):
            value = payload.get(payload_key)
            if value and key not in ids:
                ids[key] = str(value)

    return ids


def _normalize_viewer_row(result: Any) -> dict[str, Any]:
    row = normalize_row(result)
    body = str(row.get("body") or "")
    fields = row.get("fields") or {}
    ids = _ids_from_body(body)
    for key in ("session_id", "message_id", "room", "protocol", "call_id_ged", "phone_number", "reason"):
        if fields.get(key) and key not in ids:
            ids[key] = str(fields[key])
    return {
        **row,
        "container": _container_from_subject(str(row.get("subject") or "")),
        "ids": ids,
        "region": "",
    }


def _summary_with_count(summary: dict[str, Any], count: int) -> dict[str, Any]:
    return {**summary, "resultCount": count}


def _default_config_file() -> str:
    home_config = Path("~/.oci/config").expanduser()
    if home_config.exists():
        return str(home_config)
    windows_configs = sorted(Path("/mnt/c/Users").glob("*/.oci/config")) if Path("/mnt/c/Users").exists() else []
    if len(windows_configs) == 1:
        return str(windows_configs[0])
    return "~/.oci/config"


def _repo_root() -> Path:
    return Path(__file__).resolve().parents[3]


def _candidate_codex_bins(explicit: str | None) -> list[str]:
    candidates: list[str] = []
    if _clean(explicit):
        raw = _clean(explicit)
        candidates.append(raw)
        if Path("/mnt/c").exists():
            candidates.append(windows_path_to_wsl(raw))

    for name in ("codex", "codex.exe"):
        found = shutil.which(name)
        if found:
            candidates.append(found)

    extension_roots = [Path.home() / ".vscode" / "extensions"]
    windows_users = Path("/mnt/c/Users")
    if windows_users.exists():
        extension_roots.extend(path / ".vscode" / "extensions" for path in windows_users.glob("*"))

    for root in extension_roots:
        try:
            candidates.extend(
                str(path)
                for path in sorted(
                    root.glob("openai.chatgpt-*/bin/windows-x86_64/codex.exe"),
                    reverse=True,
                )
            )
        except OSError:
            continue

    unique: list[str] = []
    seen: set[str] = set()
    for candidate in candidates:
        if candidate and candidate not in seen:
            unique.append(candidate)
            seen.add(candidate)
    return unique


def _find_codex_bin(explicit: str | None) -> str | None:
    for candidate in _candidate_codex_bins(explicit):
        path = Path(candidate).expanduser()
        if path.exists():
            return str(path)
        found = shutil.which(candidate)
        if found:
            return found
    return None


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


def _codex_workdir_arg(codex_bin: str) -> str:
    root = str(_repo_root())
    if codex_bin.lower().endswith(".exe"):
        return _windows_path_from_wsl(root)
    return root


def _codex_command(request: CodexAnalyzeRequest, codex_bin: str) -> list[str]:
    command = [
        codex_bin,
        "exec",
        "--sandbox",
        "read-only",
        "--color",
        "never",
        "--skip-git-repo-check",
        "--ephemeral",
        "-C",
        _codex_workdir_arg(codex_bin),
    ]
    model = _clean(request.model)
    if model:
        command.extend(["--model", model])
    command.append("-")
    return command


app = FastAPI(title="OCI TIA Logs Viewer", version="2.0-cli")


@app.get("/")
def index() -> FileResponse:
    return FileResponse(Path(__file__).with_name("oci_logs_viewer.html"))


@app.get("/api/defaults")
def defaults() -> dict[str, Any]:
    return {
        "log_scope": DEFAULT_LOG_SCOPE,
        "region": DEFAULT_REGION,
        "agent_subject": TIA_AGENT_SUBJECT,
        "bridge_subject": TIA_BRIDGE_SUBJECT,
        "contas_namespace": CONTAS_NAMESPACE,
        "auth_mode": "cli",
        "config_file": _default_config_file(),
    }


@app.post("/api/query")
def preview_query(request: QueryPreviewRequest) -> dict[str, str]:
    try:
        query = build_search_query(request)
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    return {"query": query}


@app.post("/api/search")
def search_logs(request: LogSearchRequest) -> dict[str, Any]:
    time_start = _parse_datetime(request.time_start, "time_start")
    time_end = _parse_datetime(request.time_end, "time_end")
    if time_end <= time_start:
        raise HTTPException(status_code=400, detail="time_end precisa ser maior que time_start.")

    query_request = QueryPreviewRequest(
        **request.model_dump(exclude={"auth", "time_start", "time_end", "limit", "page", "auto_page", "max_pages"})
    )
    try:
        query = build_search_query(query_request)
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc

    cli_args = _query_args(query_request, request.auth, limit=request.limit)
    try:
        cli_args.config_file = prepare_cli_config(cli_args.config_file, cli_args.profile)
        results: list[Any] = []
        summaries: list[dict[str, Any]] = []
        next_page = request.page
        pages_fetched = 0
        while True:
            payload = run_oci_page(
                cli_args,
                query,
                _rfc3339(time_start),
                _rfc3339(time_end),
                next_page,
            )
            results.extend(extract_results(payload))
            summaries.append(extract_summary(payload))
            pages_fetched += 1
            next_page = next_page_token(payload)
            if request.page or not request.auto_page or not next_page or pages_fetched >= request.max_pages:
                break
    except RuntimeError as exc:
        raise HTTPException(
            status_code=502,
            detail=_api_error(
                "OCI CLI falhou",
                str(exc),
                "Confira o binario oci, profile/config, permissao IAM, regiao, escopo do log e sintaxe da query.",
            ),
        ) from exc
    except Exception as exc:
        raise HTTPException(
            status_code=500,
            detail=_api_error(
                "Erro local ao consultar a OCI via CLI",
                str(exc),
                "Confira caminhos de config/PEM, WSL, rede/VPN e tente novamente.",
            ),
        ) from exc

    rows = [_normalize_viewer_row(item) for item in results]
    summary = _summary_with_count(summaries[-1] if summaries else {}, len(rows))
    summary["pagesFetched"] = pages_fetched
    summary["rowsFetched"] = len(rows)
    return {
        "query": query,
        "time_start": _rfc3339(time_start),
        "time_end": _rfc3339(time_end),
        "rows": rows,
        "summary": summary,
        "headers": {
            "opc_request_id": None,
            "next_page": next_page,
        },
    }


@app.post("/api/analyze")
def analyze_with_codex(request: CodexAnalyzeRequest) -> dict[str, Any]:
    codex_bin = _find_codex_bin(request.codex_bin)
    if not codex_bin:
        raise HTTPException(
            status_code=400,
            detail=_api_error(
                "Codex CLI nao encontrado",
                "Nao encontrei o binario codex/codex.exe.",
                "Preencha o caminho do Codex CLI na tela ou confira se ele esta no PATH.",
            ),
        )

    command = _codex_command(request, codex_bin)
    env = {**os.environ, "NO_COLOR": "1"}
    try:
        completed = subprocess.run(
            command,
            input=request.prompt,
            text=True,
            capture_output=True,
            timeout=request.timeout_seconds,
            cwd=_repo_root(),
            env=env,
            check=False,
        )
    except subprocess.TimeoutExpired as exc:
        raise HTTPException(
            status_code=504,
            detail=_api_error(
                "Codex CLI excedeu o timeout",
                f"A analise passou de {request.timeout_seconds}s.",
                "Aumente o timeout ou reduza o numero de logs/char por log do pacote.",
            ),
        ) from exc
    except OSError as exc:
        raise HTTPException(
            status_code=502,
            detail=_api_error(
                "Falha ao iniciar Codex CLI",
                str(exc),
                "Confira o caminho do binario Codex CLI e permissoes do ambiente.",
            ),
        ) from exc

    stdout = completed.stdout.strip()
    stderr = completed.stderr.strip()
    if completed.returncode != 0:
        raise HTTPException(
            status_code=502,
            detail=_api_error(
                "Codex CLI falhou",
                stderr or stdout or f"Processo retornou codigo {completed.returncode}.",
                "Confira login do Codex, rede, profile/modelo e tente novamente.",
                raw="\n".join(part for part in (stdout, stderr) if part),
            ),
        )

    return {
        "analysis": stdout,
        "stderr": stderr,
        "codex_bin": codex_bin,
    }


def main() -> None:
    parser = argparse.ArgumentParser(description="Local OCI Logging/Search viewer for TIA logs using OCI CLI.")
    parser.add_argument("--host", default="127.0.0.1")
    parser.add_argument("--port", default=8765, type=int)
    args = parser.parse_args()

    import uvicorn

    uvicorn.run("app.tools.oci_logs_viewer:app", host=args.host, port=args.port, reload=False)


if __name__ == "__main__":
    main()
