from __future__ import annotations

import argparse
import json
import os
import re
import sys
from collections import Counter
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Mapping

ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))

from app.core.data_factory.n8n_client import N8nClient, N8nClientError
from app.core.data_flow.workflow_repository import workflow_hash


SCHEMA_VERSION = "1.0"
KNOWN_NODE_CATEGORIES = {
    "trigger",
    "sql",
    "python_or_script",
    "http",
    "condition",
    "notify",
    "ai",
    "subflow",
}


def _parse_timestamp(value: Any) -> datetime | None:
    if not value:
        return None
    try:
        parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
    except ValueError:
        return None
    if parsed.tzinfo is None:
        parsed = parsed.replace(tzinfo=timezone.utc)
    return parsed.astimezone(timezone.utc)


def _safe_error(value: Any) -> str:
    message = re.sub(
        r"(?i)((?:api[-_ ]?key|authorization|password|token)\s*[=:]\s*)\S+",
        r"\1[redacted]",
        str(value),
    )
    message = re.sub(
        r"(?i)(authorization\s+bearer\s+)\S+",
        r"\1[redacted]",
        message,
    )
    return message[:500]


def _node_category(node_type: str) -> str:
    normalized = node_type.lower()
    if "trigger" in normalized or normalized.endswith(".webhook"):
        return "trigger"
    if any(
        name in normalized
        for name in ("postgres", "mysql", "mssql", "mariadb", "oracle", "snowflake")
    ):
        return "sql"
    if any(
        name in normalized
        for name in ("python", ".code", "executecommand", ".ssh")
    ):
        return "python_or_script"
    if "httprequest" in normalized:
        return "http"
    if normalized.endswith((".if", ".switch", ".filter", ".merge")):
        return "condition"
    if any(
        name in normalized
        for name in ("email", "slack", "teams", "telegram", "mattermost", "discord")
    ):
        return "notify"
    if "langchain" in normalized or ".ai" in normalized:
        return "ai"
    if "executeworkflow" in normalized:
        return "subflow"
    return "unknown"


def _credential_references(nodes: list[dict[str, Any]]) -> list[dict[str, str]]:
    references = []
    for node in nodes:
        credentials = node.get("credentials")
        if not isinstance(credentials, dict):
            continue
        for credential_type, credential in credentials.items():
            if not isinstance(credential, dict):
                continue
            name = str(credential.get("name") or "").strip()
            if not name:
                continue
            references.append(
                {
                    "node_name": str(node.get("name") or ""),
                    "credential_type": str(credential_type),
                    "credential_name": name,
                }
            )
    return sorted(
        references,
        key=lambda item: (
            item["node_name"],
            item["credential_type"],
            item["credential_name"],
        ),
    )


def _trigger_summary(nodes: list[dict[str, Any]]) -> list[dict[str, str]]:
    triggers = []
    for node in nodes:
        node_type = str(node.get("type") or "")
        if _node_category(node_type) != "trigger":
            continue
        trigger = {
            "node_name": str(node.get("name") or ""),
            "node_type": node_type,
        }
        parameters = node.get("parameters")
        if isinstance(parameters, dict) and node_type.lower().endswith(".webhook"):
            path = parameters.get("path")
            method = parameters.get("httpMethod")
            if path:
                trigger["webhook_path"] = str(path)
            if method:
                trigger["http_method"] = str(method)
        triggers.append(trigger)
    return triggers


def _execution_summary(
    executions: list[dict[str, Any]], *, generated_at: datetime
) -> dict[str, Any]:
    cutoff = generated_at - timedelta(days=90)
    recent = [
        execution
        for execution in executions
        if (started_at := _parse_timestamp(execution.get("startedAt")))
        and started_at >= cutoff
    ]
    recent.sort(
        key=lambda item: _parse_timestamp(item.get("startedAt"))
        or datetime.min.replace(tzinfo=timezone.utc),
        reverse=True,
    )
    statuses = Counter(str(item.get("status") or "unknown") for item in recent)
    latest = recent[0] if recent else {}
    return {
        "count_90d": len(recent),
        "status_counts": dict(sorted(statuses.items())),
        "last_status": latest.get("status"),
        "last_started_at": latest.get("startedAt"),
        "last_stopped_at": latest.get("stoppedAt"),
    }


def _collect_pages(client, method_name: str, **kwargs) -> list[dict[str, Any]]:
    items: list[dict[str, Any]] = []
    cursor = None
    seen_cursors = set()
    for _ in range(1000):
        result = getattr(client, method_name)(
            **kwargs,
            limit=100,
            cursor=cursor,
        )
        page = result.get("data", [])
        if isinstance(page, list):
            items.extend(item for item in page if isinstance(item, dict))
        next_cursor = result.get("nextCursor")
        if not next_cursor:
            return items
        if next_cursor in seen_cursors:
            raise ValueError(f"n8n {method_name} returned a repeated cursor")
        seen_cursors.add(next_cursor)
        cursor = next_cursor
    raise ValueError(f"n8n {method_name} exceeded the page safety limit")


def empty_inventory(*, reason: str, generated_at: str | None = None) -> dict[str, Any]:
    timestamp = generated_at or datetime.now(timezone.utc).isoformat()
    return {
        "schema_version": SCHEMA_VERSION,
        "generated_at": timestamp,
        "source": "n8n",
        "collection_status": "pending_live_n8n_access",
        "collection_error": _safe_error(reason),
        "coverage": {
            "workflow_count": 0,
            "active_count": 0,
            "recently_executed_count": 0,
            "blocked_count": 0,
            "unbound_count": 0,
        },
        "workflows": [],
    }


def collect_inventory(
    client,
    *,
    bindings: Mapping[str, str] | None = None,
    generated_at: str | None = None,
) -> dict[str, Any]:
    timestamp = generated_at or datetime.now(timezone.utc).isoformat()
    generated = _parse_timestamp(timestamp)
    if generated is None:
        raise ValueError("generated_at must be an ISO 8601 timestamp")
    workflow_bindings = {str(key): str(value) for key, value in (bindings or {}).items()}
    workflows = _collect_pages(client, "list_workflows")
    inventory_workflows = []

    for workflow in workflows:
        workflow_id = str(workflow.get("id") or "").strip()
        if not workflow_id:
            continue
        executions = _collect_pages(
            client,
            "list_executions",
            workflow_id=workflow_id,
            include_data=False,
        )
        nodes = [
            node for node in workflow.get("nodes", []) if isinstance(node, dict)
        ]
        node_types = sorted(
            {str(node.get("type") or "").strip() for node in nodes if node.get("type")}
        )
        categories = sorted({_node_category(node_type) for node_type in node_types})
        dataflow_uid = workflow_bindings.get(workflow_id)
        blockers = [
            f"unsupported_node:{node_type}"
            for node_type in node_types
            if _node_category(node_type) == "unknown"
        ]
        if not dataflow_uid:
            blockers.append("missing_dataflow_binding")
        recent_execution = _execution_summary(executions, generated_at=generated)
        active = bool(workflow.get("active"))
        inventory_workflows.append(
            {
                "workflow_id": workflow_id,
                "name": str(workflow.get("name") or ""),
                "active": active,
                "updated_at": workflow.get("updatedAt"),
                "tags": sorted(
                    str(tag.get("name"))
                    for tag in workflow.get("tags", [])
                    if isinstance(tag, dict) and tag.get("name")
                ),
                "dataflow_uid": dataflow_uid,
                "definition_hash": workflow_hash(workflow),
                "node_types": node_types,
                "node_categories": categories,
                "triggers": _trigger_summary(nodes),
                "credential_references": _credential_references(nodes),
                "recent_execution": recent_execution,
                "migration_assessment": {
                    "status": "blocked" if blockers else "review_required",
                    "priority": "blocked"
                    if blockers
                    else ("high" if active else "normal"),
                    "production_write": "unknown",
                    "idempotent": "unknown",
                    "shadow_mode": "review_required",
                    "blockers": sorted(blockers),
                },
            }
        )

    inventory_workflows.sort(key=lambda item: item["workflow_id"])
    return {
        "schema_version": SCHEMA_VERSION,
        "generated_at": timestamp,
        "source": "n8n",
        "collection_status": "complete",
        "collection_error": None,
        "coverage": {
            "workflow_count": len(inventory_workflows),
            "active_count": sum(item["active"] for item in inventory_workflows),
            "recently_executed_count": sum(
                item["recent_execution"]["count_90d"] > 0
                for item in inventory_workflows
            ),
            "blocked_count": sum(
                item["migration_assessment"]["status"] == "blocked"
                for item in inventory_workflows
            ),
            "unbound_count": sum(
                item["dataflow_uid"] is None for item in inventory_workflows
            ),
        },
        "workflows": inventory_workflows,
    }


def _load_bindings(path: str | None) -> dict[str, str]:
    if not path:
        return {}
    payload = json.loads(Path(path).read_text(encoding="utf-8"))
    if not isinstance(payload, dict):
        raise ValueError("bindings file must contain a workflow-id to DataFlow-UID object")
    return {str(key): str(value) for key, value in payload.items()}


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Export a credential-safe n8n migration inventory."
    )
    parser.add_argument(
        "--output",
        default="docs/generated/n8n_migration_inventory.json",
    )
    parser.add_argument("--bindings-json")
    args = parser.parse_args()

    output = Path(args.output)
    output.parent.mkdir(parents=True, exist_ok=True)
    api_url = os.environ.get("N8N_API_URL", "")
    api_key = os.environ.get("N8N_API_KEY", "")
    if not api_url or not api_key:
        result = empty_inventory(reason="N8N_API_URL or N8N_API_KEY is not configured")
        output.write_text(
            json.dumps(result, ensure_ascii=False, indent=2) + "\n",
            encoding="utf-8",
        )
        return 2
    try:
        api_timeout = int(os.environ.get("N8N_API_TIMEOUT", "30"))
        if api_timeout <= 0:
            raise ValueError
    except ValueError:
        result = empty_inventory(reason="N8N_API_TIMEOUT must be a positive integer")
        output.write_text(
            json.dumps(result, ensure_ascii=False, indent=2) + "\n",
            encoding="utf-8",
        )
        return 2

    try:
        result = collect_inventory(
            N8nClient(api_url=api_url, api_key=api_key, timeout=api_timeout),
            bindings=_load_bindings(args.bindings_json),
        )
    except (N8nClientError, ValueError) as exc:
        result = empty_inventory(reason=str(exc))
        output.write_text(
            json.dumps(result, ensure_ascii=False, indent=2) + "\n",
            encoding="utf-8",
        )
        return 2

    output.write_text(
        json.dumps(result, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    return 0


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