import os
from pathlib import Path

import boto3
from botocore.config import Config
from botocore.exceptions import BotoCoreError, ClientError
from openai import OpenAI


def require_env(name: str) -> str:
    value = os.environ.get(name)

    if not value:
        raise RuntimeError(
            f"Не задана обязательная переменная окружения: {name}"
        )

    return value


# Object Storage
S3_BUCKET = require_env("S3_BUCKET")
S3_ENDPOINT = require_env("S3_ENDPOINT")
ACCESS_KEY_ID = require_env("ACCESS_KEY_ID")
SECRET_ACCESS_KEY = require_env("SECRET_ACCESS_KEY")
DEFAULT_REGION = os.environ.get("DEFAULT_REGION", "ru-7")

INPUT_PREFIX = "input/"
OUTPUT_KEY = "output/nginx_log_analysis_report.md"

# Локальный API vLLM
VLLM_BASE_URL = os.environ.get(
    "VLLM_BASE_URL",
    "http://127.0.0.1:8000/v1",
)

VLLM_MODEL = os.environ.get(
    "VLLM_MODEL",
    "nvidia/MiniMax-M2.7-NVFP4",
)

# Локальные каталоги
LOCAL_BASE_DIR = Path.home() / "minimax-nginx-demo"
LOCAL_INPUT_DIR = LOCAL_BASE_DIR / "downloaded"
LOCAL_OUTPUT_DIR = LOCAL_BASE_DIR / "output"
LOCAL_REPORT_PATH = (
    LOCAL_OUTPUT_DIR / "nginx_log_analysis_report.md"
)

LOCAL_INPUT_DIR.mkdir(parents=True, exist_ok=True)
LOCAL_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

# Ограничиваем размер входных данных.
# vLLM запущен с max-model-len=32768.
MAX_CHARS_PER_FILE = 80_000
MAX_TOTAL_INPUT_CHARS = 100_000

EXPECTED_FILES = {
    "analysis_task.md",
    "incident_context.md",
    "nginx_access.log",
}


s3 = boto3.client(
    "s3",
    endpoint_url=S3_ENDPOINT,
    aws_access_key_id=ACCESS_KEY_ID,
    aws_secret_access_key=SECRET_ACCESS_KEY,
    region_name=DEFAULT_REGION,
    config=Config(
        connect_timeout=30,
        read_timeout=120,
        retries={
            "max_attempts": 5,
            "mode": "standard",
        },
        s3={
            "addressing_style": "path",
        },
    ),
)


def list_input_objects() -> list[str]:
    keys: list[str] = []
    continuation_token: str | None = None

    while True:
        request = {
            "Bucket": S3_BUCKET,
            "Prefix": INPUT_PREFIX,
        }

        if continuation_token:
            request["ContinuationToken"] = continuation_token

        response = s3.list_objects_v2(**request)

        for item in response.get("Contents", []):
            key = item["Key"]

            if key.endswith("/"):
                continue

            filename = Path(key).name

            if filename in EXPECTED_FILES:
                keys.append(key)

        if not response.get("IsTruncated"):
            break

        continuation_token = response.get(
            "NextContinuationToken"
        )

    return sorted(keys)


def clear_local_input_dir() -> None:
    for file_path in LOCAL_INPUT_DIR.iterdir():
        if file_path.is_file():
            file_path.unlink()


def download_files() -> list[Path]:
    object_keys = list_input_objects()

    clear_local_input_dir()

    downloaded_files: list[Path] = []

    for key in object_keys:
        local_path = LOCAL_INPUT_DIR / Path(key).name

        print(f"Скачиваем s3://{S3_BUCKET}/{key}")

        s3.download_file(
            S3_BUCKET,
            key,
            str(local_path),
        )

        downloaded_files.append(local_path)

    return sorted(downloaded_files)


def read_file_limited(
    file_path: Path,
    max_chars: int,
) -> tuple[str, bool]:
    chunks: list[str] = []
    chars_read = 0
    truncated = False

    with file_path.open(
        "r",
        encoding="utf-8",
        errors="ignore",
    ) as file:
        while chars_read < max_chars:
            chunk_size = min(
                8192,
                max_chars - chars_read,
            )

            chunk = file.read(chunk_size)

            if not chunk:
                break

            chunks.append(chunk)
            chars_read += len(chunk)

        if file.read(1):
            truncated = True

    content = "".join(chunks)

    if truncated:
        content += (
            "\n\n"
            "[Файл обрезан для демонстрационного запроса]"
            "\n"
        )

    return content, truncated


def build_prompt(files: list[Path]) -> str:
    documents: list[str] = []
    remaining_chars = MAX_TOTAL_INPUT_CHARS

    priority = {
        "analysis_task.md": 0,
        "incident_context.md": 1,
        "nginx_access.log": 2,
    }

    sorted_files = sorted(
        files,
        key=lambda path: priority.get(path.name, 99),
    )

    for file_path in sorted_files:
        if remaining_chars <= 0:
            break

        file_limit = min(
            MAX_CHARS_PER_FILE,
            remaining_chars,
        )

        content, _ = read_file_limited(
            file_path,
            file_limit,
        )

        documents.append(
            f"## Файл: {file_path.name}\n\n{content}"
        )

        remaining_chars -= len(content)

    joined_documents = "\n\n---\n\n".join(documents)

    return f"""
Ты senior SRE/DevOps-инженер.

Проанализируй NGINX access logs и дополнительные файлы
с контекстом задачи.

Данные:

{joined_documents}

Подготовь отчет на русском языке в формате Markdown.

В отчете отрази:

1. Краткое резюме.
2. Основные паттерны запросов.
3. Частые HTTP-статусы и их возможное значение.
4. URL или группы URL, на которые стоит обратить внимание.
5. Подозрительные IP, user agents или request patterns.
6. Возможные причины проблем.
7. Что проверить системному администратору.
8. Какие метрики добавить в мониторинг.
9. Приоритетный план действий.

Требования к ответу:

- не пересказывай логи построчно;
- группируй наблюдения;
- отделяй факты из логов от гипотез;
- не делай точных выводов, если данных недостаточно;
- для каждой рекомендации указывай, на каком наблюдении
  она основана;
- учитывай, что для демонстрации файл логов мог быть обрезан.
""".strip()


def call_model(prompt: str) -> str:
    client = OpenAI(
        base_url=VLLM_BASE_URL,
        api_key="not-needed",
        timeout=600,
        max_retries=2,
    )

    response = client.chat.completions.create(
        model=VLLM_MODEL,
        messages=[
            {
                "role": "system",
                "content": (
                    "Ты технический ассистент для анализа "
                    "NGINX access logs. Отделяй подтвержденные "
                    "наблюдения от предположений и не придумывай "
                    "данные, которых нет во входных файлах."
                ),
            },
            {
                "role": "user",
                "content": prompt,
            },
        ],
        max_tokens=2500,
        temperature=0.2,
    )

    report = response.choices[0].message.content

    if not report or not report.strip():
        raise RuntimeError(
            "Модель вернула пустой ответ."
        )

    return report.strip()


def save_report_locally(report: str) -> None:
    LOCAL_REPORT_PATH.write_text(
        report,
        encoding="utf-8",
    )


def upload_report(report: str) -> None:
    s3.put_object(
        Bucket=S3_BUCKET,
        Key=OUTPUT_KEY,
        Body=report.encode("utf-8"),
        ContentType="text/markdown; charset=utf-8",
    )


def main() -> None:
    try:
        files = download_files()

        found_names = {
            file_path.name
            for file_path in files
        }

        missing_files = EXPECTED_FILES - found_names

        if missing_files:
            missing = ", ".join(
                sorted(missing_files)
            )

            raise RuntimeError(
                f"В s3://{S3_BUCKET}/{INPUT_PREFIX} "
                f"не найдены обязательные файлы: {missing}"
            )

        print(f"Загружено файлов: {len(files)}")

        for file_path in files:
            print(f"- {file_path.name}")

        prompt = build_prompt(files)

        print(
            f"Размер prompt: {len(prompt):,} символов"
        )
        print(f"Модель: {VLLM_MODEL}")
        print("Отправляем запрос в MiniMax-M2.7...")

        report = call_model(prompt)

        print("Сохраняем локальную копию отчета...")
        save_report_locally(report)

        print("Загружаем отчет в Object Storage...")
        upload_report(report)

        print("Готово.")
        print(
            f"Локальный файл: {LOCAL_REPORT_PATH}"
        )
        print(
            f"S3-объект: "
            f"s3://{S3_BUCKET}/{OUTPUT_KEY}"
        )

    except (ClientError, BotoCoreError) as error:
        raise RuntimeError(
            f"Ошибка при работе с Object Storage: {error}"
        ) from error


if __name__ == "__main__":
    main()