#!/usr/bin/env python3

"""rt-tickets: A command-line tool to query tickets on Request Tracker using its REST API v2.

This script allows filtering, paginating, and displaying tickets retrieved from RT.
"""

import argparse
import datetime
import json
import logging
import math
import os
import os.path
import shlex
import sys

import requests

log = logging.getLogger(os.path.basename(sys.argv[0]))

# Hardcoded custom field configurations
CF_CUSTOMER_NAME = "Client"
CF_SERVICE_ID = 8  # Fixed ID for the "Service" (Machine) custom field

CONFIG = {
    "RT_DEFAULT_PER_PAGE": "25",
    "RT_DEFAULT_TIME_DAYS": "1825",
}
CONFIG_PATHS = [
    os.path.abspath("/etc/eeadmtools"),
    os.path.expanduser("~/.config/eeadmtools"),
]


def load_configuration():
    """Loads configuration from /etc/eeadmtools and ~/.config/eeadmtools."""
    for path in CONFIG_PATHS:
        if os.path.isfile(path):
            try:
                with open(path, encoding="utf-8") as file_pointer:
                    for line in file_pointer:
                        line = line.strip()
                        if not line or line.startswith("#"):
                            continue
                        if line.startswith("export "):
                            line = line[7:].strip()
                        if "=" in line:
                            key, val = line.split("=", 1)
                            CONFIG[key.strip()] = val.strip().strip('"').strip("'")
            except OSError as err:
                print(f"Warning: Failed to read config {path}: {err}", file=sys.stderr)


ARGS = None


def parse_args():
    """Parse CLI arguments"""
    global ARGS  # pylint: disable=global-statement
    parser = argparse.ArgumentParser(
        description="rt-tickets: Query Request Tracker tickets via REST API v2"
    )

    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="Enable verbose mode",
    )
    parser.add_argument(
        "-d",
        "--debug",
        action="store_true",
        help="Enable debug mode",
    )

    rt_api_args = parser.add_argument_group("RT API Options")
    rt_api_args.add_argument(
        "-U", "--api-url", help="Request Tracker root URL", default=CONFIG.get("RT_URL")
    )
    rt_api_args.add_argument(
        "-T",
        "--api-token",
        help="Request Tracker authentication token",
        default=CONFIG.get("RT_TOKEN"),
    )

    filtering_args = parser.add_argument_group("Filtering Options")
    output_args = parser.add_argument_group("Output & Formatting Options")

    filtering_args.add_argument(
        "pattern",
        nargs="?",
        help="Text pattern to search within the ticket Subject (or Content if -F is set).",
    )
    filtering_args.add_argument(
        "-F",
        "--full-text",
        action="store_true",
        help="Perform a full-text search (Content) instead of a Subject-only search.",
    )
    filtering_args.add_argument(
        "-u",
        "--user",
        default="__CurrentUser__",
        help="Filter tickets owned by a specific RT user (defaults to owner of the auth token).",
    )
    filtering_args.add_argument(
        "-a",
        "--any-users",
        action="store_true",
        help="Disable the owner filter entirely to search for tickets owned by anyone.",
    )
    filtering_args.add_argument(
        "-c",
        "--customer",
        help="Filter tickets by Customer name (matches Custom Field 'Client').",
    )
    filtering_args.add_argument(
        "-q",
        "--queue",
        action="append",
        default=[],
        help="Restrict search to specific RT Queue names (can be specified multiple times).",
    )
    filtering_args.add_argument(
        "-m",
        "--machine",
        help="Filter tickets by Machine/Service label (requires -c / --customer to be provided).",
    )
    filtering_args.add_argument(
        "-s",
        "--status",
        action="append",
        default=[],
        help="Filter by status. Meta targets: 'active' (default), 'inactive', 'any'.",
    )
    filtering_args.add_argument(
        "-S",
        "--all-status",
        action="store_true",
        help="Shortcut to disable status filtering (equivalent to '-s any').",
    )
    filtering_args.add_argument(
        "-t",
        "--time",
        type=int,
        default=int(CONFIG["RT_DEFAULT_TIME_DAYS"]),
        help="Time window in days matching the LastUpdated field.",
    )

    output_args.add_argument(
        "-o",
        "--orderby",
        default="LastUpdated",
        help=(
            "Field name used to sort the API results. For Custom Fields, use strictly 'CF.[name]' "
            "format."
        ),
    )
    output_args.add_argument(
        "-r",
        "--reverse",
        action="store_true",
        help="Invert sorting direction (Note: date and numeric fields default to descending).",
    )
    output_args.add_argument(
        "-l",
        "--subject-length",
        type=int,
        help="Enforce a hard maximum character length limit for the Subject column.",
    )
    output_args.add_argument(
        "-n",
        "--nb-by-pages",
        type=int,
        default=int(CONFIG["RT_DEFAULT_PER_PAGE"]),
        help="Number of ticket items to fetch and display per pagination page.",
    )
    output_args.add_argument(
        "-p",
        "--page",
        type=int,
        default=1,
        help="Target page number to fetch (ignored if -A / --all-results is active).",
    )
    output_args.add_argument(
        "-A",
        "--all-results",
        action="store_true",
        help="Fetch and combine all available pagination pages into a single output stream.",
    )
    output_args.add_argument(
        "-f",
        "--extra-field",
        dest="extra_fields",
        action="append",
        default=[],
        help=(
            "Inject extra fields or Custom Fields as new table columns. "
            "For Custom Fields, use strictly 'CF.[name]'."
        ),
    )
    output_args.add_argument(
        "-j",
        "--json",
        action="store_true",
        help="Output a structured, human-cleaned JSON list mapped on the table layout.",
    )
    output_args.add_argument(
        "--raw-json",
        action="store_true",
        help="Output the raw, unfiltered JSON dataset directly as transmitted by the RT server.",
    )

    ARGS = parser.parse_args()

    if not ARGS.api_url or not ARGS.api_token:
        parser.error("RT URL and token must be provided.")

    if ARGS.orderby.startswith("CF."):
        parser.error("Sorting by custom field value is not supported.")


def compute_curl_command(request):
    """Compute equivalent curl command from a request"""
    curl_args = ["curl", f"-X {shlex.quote(request.method)}"]
    for header, val in request.headers.items():
        curl_args.append(f"-H {shlex.quote(f'{header}: {val}')}")
    if request.body:
        curl_args.append(
            shlex.join(
                [
                    "-d",
                    (
                        request.body.decode("utf-8", errors="ignore")
                        if isinstance(request.body, bytes)
                        else str(request.body)
                    ),
                ]
            )
        )
    curl_args.append(request.url)
    return " \\\n  ".join(curl_args)


SESSION = requests.Session()


def call_rt_api(request_name, relative_url, method="GET", **kwargs):
    """Call RT API"""
    if not SESSION.headers.get("Authorization"):
        SESSION.headers.update(
            {
                "Authorization": f"token {ARGS.api_token}",
                "Accept": "application/json",
            }
        )
    url = f"{ARGS.api_url.rstrip('/')}/REST/2.0/{relative_url}"

    # Process and format logs if DEBUG level is active
    log.debug("[%s]: %s %s", request_name, method, url)
    if kwargs.get("params"):
        log.debug("[%s]: Parameters: %s", request_name, json.dumps(kwargs["params"]))
    if kwargs.get("json"):
        log.debug("[%s]: JSON data: %s", request_name, json.dumps(kwargs["json"]))

    request = SESSION.prepare_request(requests.Request(method, url, **kwargs))
    log.debug("[%s]: Equivalent curl:\n%s", request_name, compute_curl_command(request))

    try:
        result = SESSION.send(request, timeout=CONFIG.get("RT_TIMEOUT", 30))
        log.debug("[%s]: HTTP response code: %s", request_name, result.status_code)

        if result.status_code != 200:
            log.error("RT API returned HTTP %s on query %s", result.status_code, request_name)
            log.error("Full API Response Content:\n%s", result.text)
            sys.exit(1)
        return result.json()
    except requests.exceptions.RequestException as err:
        log.error("Network issue on query %s: %s", request_name, err)
        sys.exit(1)


def get_machine_id(machine_label):
    """Queries the RT API to map a machine name to its ID.

    Args:
        machine_label (str): Label of the target machine.

    Returns:
        str: The ID of the machine.
    """
    try:
        return int(machine_label)
    except ValueError:
        pass
    log.info("Get machine '%s' ID...", machine_label)
    params = {"description": machine_label}
    if ARGS.customer:
        params["category"] = ARGS.customer
    values = call_rt_api(
        f"GET MACHINE '{machine_label}' ID",
        f"customfield/{CF_SERVICE_ID}",
        params=params,
    ).get("Values", [])
    log.info("%d machine(s) found for label pattern '%s'", len(values), machine_label)
    if not values:
        log.error("Machine '%s' not found.", machine_label)
        sys.exit(1)
    if len(values) > 1:
        log.debug("Filter machines found with exact name...")
        # Filter on exact machine name
        exact_name_values = [value for value in values if value["description"] == machine_label]
        log.info("%s machine(s) found with exact name '%s'", len(exact_name_values), machine_label)
        if len(exact_name_values) == 1:
            values = exact_name_values
        else:
            log.error(
                "Multiple machines found with name '%s':\n%s\nSpecify it by %s.",
                machine_label,
                "\n".join(
                    f"- '{value['description']}' [#{value['name']} - {value['category']}]"
                    for value in values
                ),
                (
                    "its ID"
                    if exact_name_values and ARGS.customer
                    else (
                        "its exact name or ID"
                        if ARGS.customer
                        else "customer ID and its exact name or ID"
                    )
                ),
            )
            sys.exit(1)
    log.info(
        "Machine '%s' resolved as '%s' [#%s - %s]",
        machine_label,
        values[0]["description"],
        values[0]["name"],
        values[0]["category"],
    )
    return values[0]["name"]


def get_field_value(item, field_name):
    """Extracts a simple string or resolves a nested dictionary structure.

    Args:
        item (dict): The root dictionary of the ticket object.
        field_name (str): The name of the field to extract.

    Returns:
        str: The cleaned string value.
    """
    val = item.get(field_name, "")
    if isinstance(val, dict):
        return val.get("label", val.get("id", str(val)))
    return str(val)


def extract_dynamic_field(ticket, field_name):
    """
    Dynamically extracts a core field or a nested Custom Field (CF) using strict CF.[name] format.

    Args:
        ticket (dict): The raw ticket object returned by the API.
        field_name (str): The field name to inspect (e.g., 'Status' or 'CF.MyField').

    Returns:
        str: The extracted string value.
    """
    if field_name in ticket:
        return get_field_value(ticket, field_name)

    if field_name.startswith("CF."):
        raw_cf = [cf for cf in ticket.get("CustomFields", []) if cf["name"] == field_name[3:]]
        if raw_cf:
            return ", ".join(raw_cf[0]["values"])

    return ""


_QUEUES_NAME_FROM_ID = {}


def resolve_queue_name(queue_field):
    """Extracts a queue ID and uses a local cache to resolve its human-readable name."""
    if isinstance(queue_field, dict):
        queue_id = str(queue_field.get("id", ""))
    else:
        queue_id = str(queue_field)

    if not queue_id:
        return "Unknown"
    if queue_id in _QUEUES_NAME_FROM_ID:
        return _QUEUES_NAME_FROM_ID[queue_id]

    _QUEUES_NAME_FROM_ID[queue_id] = call_rt_api(
        f"GET QUEUE #{queue_id} NAME",
        f"queue/{queue_id}",
    )["Name"]
    log.debug("Queue ID %s resolved as '%s'", queue_id, _QUEUES_NAME_FROM_ID[queue_id])
    return _QUEUES_NAME_FROM_ID[queue_id]


def display_tickets_table(tickets):
    """Generates an ASCII table dynamically sized according to the terminal width."""

    # Determine dynamic column exclusions based on explicit filtering constraints
    hide_customer = ARGS.customer is not None
    hide_owner = not ARGS.any_users

    # Status column is only hidden if we filter by a single unique explicit status (e.g., -s open)
    # Lifecycle groups ('active', 'inactive') return various status names, so column remains visible
    hide_status = (
        len(ARGS.status) == 1
        and ARGS.status[0] not in ("active", "inactive", "any")
        and not ARGS.all_status
    )

    headers_list = ["ID", "Subject"]
    if not hide_customer:
        headers_list.append("Customer")
    headers_list.extend(["Queue", "Created", "Last Updated"])
    if not hide_status:
        headers_list.append("Status")
    if not hide_owner:
        headers_list.append("Owner")
    headers_list.extend(ARGS.extra_fields)

    rows = []
    for ticket in tickets:
        row = {
            "ID": str(ticket.get("id", "")),
            "Subject": ticket.get("Subject", "") or "",
            "Queue": (resolve_queue_name(ticket["Queue"]) if ticket.get("Queue") else ""),
            "Created": ticket.get("Created", "").replace("T", " ").split(".")[0].rstrip("Z"),
            "Last Updated": ticket.get("LastUpdated", "")
            .replace("T", " ")
            .split(".")[0]
            .rstrip("Z"),
        }
        if not hide_customer:
            row["Customer"] = extract_dynamic_field(ticket, f"CF.{CF_CUSTOMER_NAME}")
        if not hide_status:
            row["Status"] = ticket.get("Status", "")
        if not hide_owner:
            row["Owner"] = get_field_value(ticket, "Owner")
        for field in ARGS.extra_fields:
            row[field] = extract_dynamic_field(ticket, field)
        rows.append(row)

    # Compute columns widths
    try:
        terminal_width = os.get_terminal_size().columns
    except OSError:
        terminal_width = 160
    col_widths = {
        header: max([len(header) + 2] + [len(row[header]) + 2 for row in rows])
        for header in headers_list
        if header != "Subject"
    }
    col_widths["Subject"] = ARGS.subject_length or max(
        terminal_width - len(headers_list) - 1 - sum(col_widths.values()), 15
    )

    # Print output
    def format_cell(name, value):
        if len(value) > col_widths[name] - 2:
            value = value[: max(1, col_widths[name] - 3)] + "…"
        return f" {value.ljust(col_widths[name] - 2)} "

    separator = "+" + "+".join(["-" * (col_widths[field]) for field in headers_list]) + "+"
    print(
        "\n".join(
            [
                separator,
                f"|{'|'.join(format_cell(field, field) for field in headers_list)}|",
                separator,
            ]
            + [
                f"|{'|'.join(format_cell(field, row[field]) for field in headers_list)}|"
                for row in rows
            ]
            + [separator]
        )
    )
    return col_widths["Subject"]


def format_json_output(tickets):
    """Formats the retrieved tickets into a structured and cleaned JSON list."""
    output = []
    for ticket in tickets:
        cf_dict = {cf["name"]: cf["values"] for cf in ticket.get("CustomFields", {})}
        output.append(
            {
                "id": str(ticket.get("id", "")),
                "Subject": ticket.get("Subject", "") or "",
                "Customer": (
                    cf_dict[CF_CUSTOMER_NAME][0] if cf_dict.get(CF_CUSTOMER_NAME) else None
                ),
                "Queue": resolve_queue_name(ticket.get("Queue")),
                "Created": ticket.get("Created", "").replace("T", " ").split(".")[0].rstrip("Z"),
                "LastUpdated": ticket.get("LastUpdated", "")
                .replace("T", " ")
                .split(".")[0]
                .rstrip("Z"),
                "Status": ticket.get("Status", ""),
                "Owner": get_field_value(ticket, "Owner"),
                "CustomFields": cf_dict,
                **{field: extract_dynamic_field(ticket, field) for field in ARGS.extra_fields},
            }
        )
    return output


def build_query_status():
    """Builds the TicketSQL clause related to status filters using native lifecycle groups."""
    if ARGS.all_status or "any" in ARGS.status:
        return "", "any status"

    if not ARGS.status:
        return "Status = '__Active__'", "active statuses (default lifecycle)"

    queries = []
    descriptions = []
    for stat in ARGS.status:
        if stat == "active":
            queries.append("Status = '__Active__'")
            descriptions.append("active")
        elif stat == "inactive":
            queries.append("Status = '__Inactive__'")
            descriptions.append("inactive")
        else:
            queries.append(f"Status = '{stat}'")
            descriptions.append(f"'{stat}'")

    if len(queries) == 1:
        return queries[0], descriptions[0]
    return f"({' OR '.join(queries)})", ", ".join(descriptions)


def main():
    """Main entry point for CLI execution."""
    load_configuration()
    parse_args()

    # Configure logging using standard library (output strictly directed to stderr)
    logging.basicConfig(
        level=logging.DEBUG if ARGS.debug else logging.INFO if ARGS.verbose else logging.WARNING,
        format="%(asctime)s - %(module)s:%(lineno)d - %(levelname)s - %(message)s",
        stream=sys.stderr,
    )

    query_parts = []
    if not ARGS.any_users:
        query_parts.append(f"Owner = '{ARGS.user}'")
    if ARGS.queue:
        query_parts.append(f"({' OR '.join([f'Queue = {q!r}' for q in ARGS.queue])})")

    status_query, status_desc = build_query_status()
    if status_query:
        query_parts.append(status_query)

    if ARGS.customer:
        query_parts.append(f"'CF.{{{CF_CUSTOMER_NAME}}}' = '{ARGS.customer}'")
    if ARGS.machine:
        machine_id = get_machine_id(ARGS.machine)
        query_parts.append(f"'CF.{{Service}}' = '{machine_id}'")
    if ARGS.pattern:
        escaped = ARGS.pattern.replace("'", "\\'")
        field = "Content" if ARGS.full_text else "Subject"
        query_parts.append(f"{field} LIKE '{escaped}'")

    target_date = datetime.datetime.now() - datetime.timedelta(days=ARGS.time)
    query_parts.append(f"LastUpdated > '{target_date.strftime('%Y-%m-%d %H:%M:%S')}'")

    requested_fields = {
        "id",
        "Subject",
        "Status",
        "Queue",
        "Created",
        "LastUpdated",
        "Owner",
        "CustomFields",
    }
    for field in ARGS.extra_fields:
        if not field.startswith("CF."):
            requested_fields.add(field)

    params = {
        "query": " AND ".join(query_parts),
        "orderby": ARGS.orderby,
        "order": (
            ("ASC" if ARGS.reverse else "DESC")
            if ARGS.orderby in ["id", "Created", "LastUpdated"]
            else ("DESC" if ARGS.reverse else "ASC")
        ),
        "fields": ",".join(requested_fields),
        "per_page": ARGS.nb_by_pages,
    }

    tickets = []
    total_items = 0
    current_page = 1 if ARGS.all_results else ARGS.page

    try:
        while True:
            log.info("Search ticket (page #%d)...", current_page)
            params["page"] = current_page

            payload = call_rt_api(
                f"SEARCH TICKETS (page: {current_page})",
                "tickets",
                params=params,
            )
            page_items = payload.get("items", [])
            tickets.extend(page_items)
            total_items = payload.get("total", 0)
            per_p = payload.get("per_page", ARGS.nb_by_pages)

            if (
                not ARGS.all_results
                or not page_items
                or len(page_items) < per_p
                or len(tickets) >= total_items
            ):
                break
            current_page += 1
    except requests.exceptions.RequestException as err:
        log.error("Network error while connecting to RT API: %s", err)
        sys.exit(1)

    if ARGS.json or ARGS.raw_json:
        print(
            json.dumps(
                (tickets if ARGS.raw_json else format_json_output(tickets)),
                indent=4,
                ensure_ascii=False,
            )
        )
        return

    if tickets:
        subject_length = display_tickets_table(tickets)
        if ARGS.all_results:
            print(f"\n{total_items} ticket(s) found.\n")
        else:
            total_pages = math.ceil(total_items / ARGS.nb_by_pages) if ARGS.nb_by_pages > 0 else 1
            print(f"\nPage {ARGS.page} on {total_pages} - {total_items} ticket(s) found.")
    else:
        print("No matched ticket found.")
        subject_length = 0

    print(
        "\n"
        f"Search parameters:\n"
        f"  [pattern] Text search pattern: {ARGS.pattern or 'none'}\n"
        f"  [-F] Full-text search enabled: {'Yes' if ARGS.full_text else 'No'}\n"
        f"  [-c] Customer: {ARGS.customer or 'all (column injected)'}\n"
        f"  [-q] Queues specified: {', '.join(ARGS.queue) if ARGS.queue else 'all'}\n"
        f"  [-o] Order by column: {ARGS.orderby}\n"
        f"  [-r] Reverse sorting order: {'Yes' if ARGS.reverse else 'No'}\n"
        f"  [-l] Max subject column length: {subject_length}"
        f"{' (dynamically optimized)' if not ARGS.subject_length else ''}\n"
        "  [-f] Requested extra columns: "
        f"{', '.join(ARGS.extra_fields) if ARGS.extra_fields else 'none'}\n"
        f"  [-n] Items per page: {ARGS.nb_by_pages}\n"
        f"  [-p] Target page: {ARGS.page if not ARGS.all_results else 'N/A'}\n"
        f"  [-A] Fetch all results combined: {'Yes' if ARGS.all_results else 'No'}\n"
        f"  [-m] Machine / Service: {ARGS.machine or 'all'}\n"
        f"  [-s/-S] Status criteria: {status_desc}\n"
        f"  [-t] Modification time window: {ARGS.time} days\n"
        f"  [-u/-a] Ticket Owner constraint: {ARGS.user if not ARGS.any_users else 'any'}"
    )


if __name__ == "__main__":
    main()
