#!/usr/bin/env python3
"""
CI Trace Report Script - Reports CI Trace data to AgentLoop from Gitea Actions workflows.

Usage in CI workflow jobs:
  - At start: python3 scripts/ci/ci_trace_report.py --status running
  - At end: python3 scripts/ci/ci_trace_report.py --status ok --start-time $CI_TRACE_START_TIME

Environment variables (built-in Gitea Actions):
  GITEA_REPOSITORY / GITHUB_REPOSITORY - repository (owner/repo)
  GITEA_WORKFLOW / GITHUB_WORKFLOW     - workflow name
  GITEA_JOB / GITHUB_JOB               - job ID
  GITEA_SHA / GITHUB_SHA               - commit SHA
  GITEA_REF_NAME / GITHUB_REF_NAME     - branch name
  GITEA_RUN_ID / GITHUB_RUN_ID         - run ID
  GITEA_ACTOR / GITHUB_ACTOR           - trigger actor
  GITEA_EVENT_NAME / GITHUB_EVENT_NAME - event type
  PR_NUMBER / GITEA_PR_NUMBER          - PR number (if PR triggered)

AgentLoop configuration (injected via Secrets):
  AGENTLOOP_LICENSE_KEY - LicenseKey (required)
  AGENTLOOP_ENDPOINT    - Trace endpoint (optional, has default)
  AGENTLOOP_PROJECT     - SLS Project name (optional)
  AGENTLOOP_WORKSPACE   - CMS Workspace name (optional)
"""

import argparse
import json
import os
import sys
import time
import urllib.error
import urllib.request
import uuid

# ========== Default Configuration ==========
DEFAULT_ENDPOINT = "https://proj-xtrace-495e81719a1fd9a2c5fd671eefafbe-cn-hangzhou.cn-hangzhou.log.aliyuncs.com/apm/trace/opentelemetry/v1/traces"
DEFAULT_PROJECT = "proj-xtrace-495e81719a1fd9a2c5fd671eefafbe-cn-hangzhou"
DEFAULT_WORKSPACE = "agentloop-13b8d6efb7fde6e9b193eb982ade68e2"


# ========== OTLP Protobuf Manual Encoding ==========


def _encode_varint(value):
    result = bytearray()
    while value > 0x7F:
        result.append((value & 0x7F) | 0x80)
        value >>= 7
    result.append(value & 0x7F)
    return bytes(result)


def _encode_tag(field_number, wire_type):
    return _encode_varint((field_number << 3) | wire_type)


def _encode_string_field(field_number, value):
    value_bytes = value.encode("utf-8")
    return _encode_tag(field_number, 2) + _encode_varint(len(value_bytes)) + value_bytes


def _encode_bytes_field(field_number, value_bytes):
    return _encode_tag(field_number, 2) + _encode_varint(len(value_bytes)) + value_bytes


def _encode_int_field(field_number, value):
    return _encode_tag(field_number, 0) + _encode_varint(value & 0xFFFFFFFFFFFFFFFF)


def _encode_message_field(field_number, message_bytes):
    return _encode_tag(field_number, 2) + _encode_varint(len(message_bytes)) + message_bytes


def _encode_key_value(key, value_str):
    any_value = _encode_string_field(1, value_str)
    return _encode_string_field(1, key) + _encode_message_field(2, any_value)


def _encode_status(status_code, status_msg=""):
    data = _encode_int_field(1, status_code)
    if status_msg:
        data += _encode_string_field(2, status_msg)
    return data


def _encode_span(
    trace_id_bytes,
    span_id_bytes,
    parent_span_id_bytes,
    name,
    start_time_unix_nano,
    end_time_unix_nano,
    span_kind,
    attributes,
    status_code,
    status_msg="",
):
    data = b""
    data += _encode_bytes_field(1, trace_id_bytes)
    data += _encode_bytes_field(2, span_id_bytes)
    if parent_span_id_bytes:
        data += _encode_bytes_field(3, parent_span_id_bytes)
    data += _encode_string_field(4, name)
    data += _encode_int_field(5, span_kind)
    data += _encode_int_field(6, start_time_unix_nano)
    data += _encode_int_field(7, end_time_unix_nano)
    for key, value in attributes.items():
        kv = _encode_key_value(key, str(value))
        data += _encode_message_field(9, kv)
    status = _encode_status(status_code, status_msg)
    data += _encode_message_field(12, status)
    return data


def _encode_resource_spans(service_name, scope_spans_bytes):
    svc_kv = _encode_key_value("service.name", service_name)
    resource = _encode_message_field(1, svc_kv)
    data = _encode_message_field(1, resource)
    data += _encode_message_field(2, scope_spans_bytes)
    return data


def _encode_scope_spans(scope_name, spans_bytes_list):
    scope = _encode_string_field(1, scope_name)
    data = _encode_message_field(1, scope)
    for span_bytes in spans_bytes_list:
        data += _encode_message_field(2, span_bytes)
    return data


def _encode_traces_data(resource_spans_bytes_list):
    data = b""
    for rs_bytes in resource_spans_bytes_list:
        data += _encode_message_field(1, rs_bytes)
    return data


# ========== Helper Functions ==========


def _gen_trace_id():
    return uuid.uuid4().bytes


def _gen_span_id():
    return uuid.uuid4().bytes[:8]


def _env(name, default=""):
    """Get env var with GITEA_/GITHUB_ prefix fallback."""
    val = os.getenv(name, "")
    if val:
        return val
    if name.startswith("GITEA_"):
        alt = "GITHUB_" + name[6:]
        return os.getenv(alt, default)
    if name.startswith("GITHUB_"):
        alt = "GITEA_" + name[7:]
        return os.getenv(alt, default)
    return default


def _get_pr_number():
    """Get PR number from environment or event file."""
    pr = os.getenv("PR_NUMBER", "") or os.getenv("GITEA_PR_NUMBER", "")
    if pr:
        return pr

    event_path = os.getenv("GITHUB_EVENT_PATH", "") or os.getenv("GITEA_EVENT_PATH", "")
    if event_path and os.path.isfile(event_path):
        try:
            with open(event_path, "r") as f:
                event = json.load(f)
            if "pull_request" in event and "number" in event["pull_request"]:
                return str(event["pull_request"]["number"])
        except Exception:
            pass

    return ""


def _get_ci_attributes():
    """Collect attributes from CI environment variables."""
    attrs = {
        "ci.repo": _env("GITEA_REPOSITORY") or _env("GITHUB_REPOSITORY") or "unknown",
        "ci.workflow": _env("GITEA_WORKFLOW") or _env("GITHUB_WORKFLOW") or "unknown",
        "ci.job": _env("GITEA_JOB") or _env("GITHUB_JOB") or "unknown",
        "ci.commit_sha": _env("GITEA_SHA") or _env("GITHUB_SHA") or "unknown",
        "ci.branch": _env("GITEA_REF_NAME") or _env("GITHUB_REF_NAME") or "unknown",
        "ci.run_id": _env("GITEA_RUN_ID") or _env("GITHUB_RUN_ID") or "unknown",
        "ci.actor": _env("GITEA_ACTOR") or _env("GITHUB_ACTOR") or "unknown",
        "ci.event": _env("GITEA_EVENT_NAME") or _env("GITHUB_EVENT_NAME") or "unknown",
    }
    pr = _get_pr_number()
    if pr:
        attrs["ci.pr_number"] = pr
    return attrs


# ========== Trace Building & Reporting ==========


def build_trace(service_name, trace_name, status, duration_ms, attributes=None):
    """Build an OTLP trace payload (protobuf bytes). No external dependencies."""
    trace_id = _gen_trace_id()
    end_time = int(time.time() * 1e9)
    start_time = end_time - int(duration_ms * 1e6)
    status_code = 1 if status in ("ok", "running") else 2
    status_msg = "" if status in ("ok", "running") else "Job failed"

    main_attrs = {
        "agent.trace_name": trace_name,
        "agent.service": service_name,
        "ci.trace_status": status,
    }
    if attributes:
        main_attrs.update(attributes)

    main_span = _encode_span(
        trace_id_bytes=trace_id,
        span_id_bytes=_gen_span_id(),
        parent_span_id_bytes=b"",
        name=trace_name,
        start_time_unix_nano=start_time,
        end_time_unix_nano=end_time,
        span_kind=1,
        attributes=main_attrs,
        status_code=status_code,
        status_msg=status_msg,
    )

    scope_spans = _encode_scope_spans("ci-trace", [main_span])
    resource_spans = _encode_resource_spans(service_name, scope_spans)
    return _encode_traces_data([resource_spans])


def report_ci_trace(
    service_name,
    trace_name,
    status="ok",
    duration_ms=1000,
    endpoint=None,
    license_key=None,
    project=None,
    workspace=None,
    extra_attributes=None,
):
    """
    Report CI Trace data. Returns (success: bool, message: str).
    Never raises exceptions; returns False on failure.
    """
    try:
        endpoint = endpoint or os.getenv("AGENTLOOP_ENDPOINT", DEFAULT_ENDPOINT)
        license_key = license_key or os.getenv("AGENTLOOP_LICENSE_KEY", "")
        project = project or os.getenv("AGENTLOOP_PROJECT", DEFAULT_PROJECT)
        workspace = workspace or os.getenv("AGENTLOOP_WORKSPACE", DEFAULT_WORKSPACE)

        if not license_key:
            return False, "[Trace] skipped: AGENTLOOP_LICENSE_KEY not configured"

        attrs = _get_ci_attributes()
        if extra_attributes:
            attrs.update(extra_attributes)

        payload = build_trace(
            service_name=service_name,
            trace_name=trace_name,
            status=status,
            duration_ms=duration_ms,
            attributes=attrs,
        )

        headers = {
            "Content-Type": "application/x-protobuf",
            "x-arms-license-key": license_key,
            "x-arms-project": project,
            "x-cms-workspace": workspace,
        }

        req = urllib.request.Request(endpoint, data=payload, headers=headers, method="POST")
        try:
            with urllib.request.urlopen(req, timeout=10) as resp:
                status_code = resp.status
                resp_body = resp.read().decode("utf-8", errors="replace")
        except urllib.error.HTTPError as e:
            status_code = e.code
            resp_body = e.read().decode("utf-8", errors="replace")

        if status_code in (200, 202):
            return True, (f"[Trace] success: {service_name} / {trace_name} " f"({status}, {duration_ms}ms)")
        else:
            return False, (f"[Trace] failed: HTTP {status_code} - {resp_body[:200]}")
    except Exception as e:
        return False, f"[Trace] error: {type(e).__name__}: {str(e)}"


def main():
    parser = argparse.ArgumentParser(description="CI AgentLoop Trace Reporter")
    parser.add_argument(
        "--service",
        dest="service_name",
        default=os.getenv("TRACE_SERVICE", ""),
        help="Service name (also via TRACE_SERVICE env)",
    )
    parser.add_argument(
        "--name",
        dest="trace_name",
        default=os.getenv("TRACE_NAME", ""),
        help="Trace name (also via TRACE_NAME env)",
    )
    parser.add_argument(
        "--status",
        default=os.getenv("TRACE_STATUS", "ok"),
        choices=["ok", "error", "running"],
        help="Status: ok / error / running (default ok)",
    )
    parser.add_argument(
        "--start-time",
        dest="start_time",
        default=os.getenv("TRACE_START_TIME", ""),
        help="Start timestamp (seconds) for duration calculation",
    )
    parser.add_argument(
        "--duration-ms",
        dest="duration_ms",
        type=int,
        default=0,
        help="Direct duration in ms; takes precedence over --start-time",
    )
    parser.add_argument("--attrs", default="", help="Extra attributes (JSON string)")

    args = parser.parse_args()

    if not args.service_name:
        print("[Trace] skipped: no service specified (--service or TRACE_SERVICE)")
        sys.exit(0)

    duration_ms = args.duration_ms
    if duration_ms <= 0 and args.start_time:
        try:
            start_ts = float(args.start_time)
            duration_ms = int((time.time() - start_ts) * 1000)
        except (ValueError, TypeError):
            duration_ms = 1000
    if duration_ms <= 0:
        duration_ms = 1000

    extra_attrs = {}
    if args.attrs:
        try:
            extra_attrs = json.loads(args.attrs)
        except json.JSONDecodeError:
            pass

    trace_name = args.trace_name
    if not trace_name:
        wf = _env("GITEA_WORKFLOW") or _env("GITHUB_WORKFLOW") or "CI"
        job = _env("GITEA_JOB") or _env("GITHUB_JOB") or "job"
        trace_name = f"{wf} / {job}"

    success, msg = report_ci_trace(
        service_name=args.service_name,
        trace_name=trace_name,
        status=args.status,
        duration_ms=duration_ms,
        extra_attributes=extra_attrs,
    )

    print(msg)
    sys.exit(0)


if __name__ == "__main__":
    main()
