# Copyright 2025 Softwell S.r.l.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Interactive REPL for genro-asgi remote CLI.

Usage:
    genro-cli connect ws://host:port [--user USER --password PASS] [--token TOKEN]

REPL commands:
    list                     — show discovered endpoints
    list <query>             — filter endpoints by path/summary
    GET /path                — invoke endpoint
    POST /path {"data":...}  — invoke with payload
    help /path               — show endpoint details
    ping                     — WSX ping/pong
    refresh                  — re-fetch endpoint list
    quit                     — close connection and exit
"""

from __future__ import annotations

import argparse
import asyncio
import json
import logging
import sys
from typing import Any

from .connection import WsxConnection
from .discovery import EndpointDiscovery
from .formatter import OutputFormatter

__all__ = ["main"]

logger = logging.getLogger("genro_cli")

HELP_TEXT = """
Commands:
  list [query]               List endpoints (optionally filtered)
  GET /path [?key=val]       Invoke GET endpoint
  POST /path {"data":...}    Invoke POST with JSON body
  PUT /path {"data":...}     Invoke PUT with JSON body
  DELETE /path               Invoke DELETE endpoint
  help /path [METHOD]        Show endpoint details
  ping                       WSX ping/pong
  refresh                    Re-fetch endpoint list
  quit / exit                Close connection
  help                       Show this message
"""

HTTP_METHODS = {"GET", "POST", "PUT", "DELETE", "PATCH"}


class Repl:
    """Interactive REPL session."""

    __slots__ = ("connection", "discovery", "formatter")

    def __init__(self, connection: WsxConnection) -> None:
        self.connection = connection
        self.discovery = EndpointDiscovery(connection)
        self.formatter = OutputFormatter()

    async def run(self) -> None:
        """Connect, discover, and enter the REPL loop."""
        await self.connection.connect()
        print(f"Connected to {self.connection.url}")

        try:
            endpoints = await self.discovery.discover()
            print(f"Discovered {len(endpoints)} endpoints. Type 'help' for commands.")
        except Exception as exc:
            print(f"Warning: endpoint discovery failed: {exc}")

        await self._loop()

    async def _loop(self) -> None:
        """Main input loop."""
        try:
            while self.connection.connected:
                try:
                    line = await asyncio.get_event_loop().run_in_executor(
                        None, lambda: input("genro> ")
                    )
                except EOFError:
                    break

                line = line.strip()
                if not line:
                    continue

                try:
                    await self._dispatch(line)
                except KeyboardInterrupt:
                    print()
                    continue
                except Exception as exc:
                    print(f"Error: {exc}")
        finally:
            await self.connection.close()

    async def _dispatch(self, line: str) -> None:
        """Route user input to the appropriate handler."""
        parts = line.split(None, 1)
        cmd = parts[0].lower()
        rest = parts[1] if len(parts) > 1 else ""

        if cmd in ("quit", "exit"):
            await self.connection.close()
            return

        if cmd == "help":
            if rest:
                await self._cmd_help_endpoint(rest)
            else:
                print(HELP_TEXT)
            return

        if cmd == "list":
            self._cmd_list(rest)
            return

        if cmd == "ping":
            await self._cmd_ping()
            return

        if cmd == "refresh":
            await self._cmd_refresh()
            return

        # HTTP method invocation
        if cmd.upper() in HTTP_METHODS:
            await self._cmd_invoke(cmd.upper(), rest)
            return

        print(f"Unknown command: {cmd}. Type 'help' for available commands.")

    def _cmd_list(self, query: str) -> None:
        """List endpoints, optionally filtered."""
        if query:
            endpoints = self.discovery.search(query)
        else:
            endpoints = self.discovery.endpoints

        if not endpoints:
            print("No endpoints found.")
            return

        for ep in endpoints:
            print(f"  {ep.display_name}")

    async def _cmd_ping(self) -> None:
        """Send WSX ping."""
        response = await self.connection.ping()
        print(f"pong (status={response.get('status')})")

    async def _cmd_refresh(self) -> None:
        """Re-fetch endpoint list."""
        endpoints = await self.discovery.refresh()
        print(f"Refreshed: {len(endpoints)} endpoints")

    async def _cmd_invoke(self, method: str, rest: str) -> None:
        """Invoke an endpoint."""
        path, query, data = self._parse_invocation(rest)
        response = await self.connection.request(
            method=method, path=path, query=query, data=data,
        )
        print(self.formatter.format_response(response))

    async def _cmd_help_endpoint(self, rest: str) -> None:
        """Show details for a specific endpoint."""
        parts = rest.split()
        path = parts[0]
        method = parts[1].upper() if len(parts) > 1 else "GET"

        ep = self.discovery.get_endpoint(path, method)
        if not ep:
            # Try all methods for this path
            matches = [e for e in self.discovery.endpoints if e.path == path]
            if matches:
                print(f"Available methods for {path}:")
                for e in matches:
                    print(f"  {e.display_name}")
            else:
                print(f"Endpoint not found: {method} {path}")
            return

        print(f"  {ep.method} {ep.path}")
        if ep.summary:
            print(f"  Summary: {ep.summary}")
        if ep.description:
            print(f"  Description: {ep.description}")
        if ep.tags:
            print(f"  Tags: {', '.join(ep.tags)}")
        if ep.parameters:
            print("  Parameters:")
            for p in ep.parameters:
                required = " (required)" if p.get("required") else ""
                print(f"    {p.get('name', '?')}: {p.get('schema', {}).get('type', '?')}{required}")
        if ep.request_body:
            print("  Request body: yes")

    def _parse_invocation(self, rest: str) -> tuple[str, dict[str, Any] | None, Any]:
        """Parse path, query params, and body from invocation string.

        Formats:
            /path
            /path?key=val&key2=val2
            /path {"json": "body"}
            /path?key=val {"json": "body"}
        """
        query: dict[str, Any] | None = None
        data: Any = None

        # Split off JSON body if present
        json_start = rest.find("{")
        if json_start >= 0:
            path_part = rest[:json_start].strip()
            json_part = rest[json_start:]
            try:
                data = json.loads(json_part)
            except json.JSONDecodeError as exc:
                raise ValueError(f"Invalid JSON body: {exc}") from exc
        else:
            path_part = rest.strip()

        # Split path and query string
        if "?" in path_part:
            path, qs = path_part.split("?", 1)
            query = {}
            for pair in qs.split("&"):
                if "=" in pair:
                    k, v = pair.split("=", 1)
                    query[k] = v
        else:
            path = path_part

        if not path.startswith("/"):
            path = "/" + path

        return path, query, data


def _build_parser() -> argparse.ArgumentParser:
    """Build CLI argument parser."""
    parser = argparse.ArgumentParser(
        prog="genro-cli",
        description="Remote CLI for genro-asgi servers",
    )
    sub = parser.add_subparsers(dest="command")

    connect = sub.add_parser("connect", help="Connect to a server")
    connect.add_argument("url", help="WebSocket URL (ws://host:port)")
    connect.add_argument("--user", "-u", help="Username for basic auth")
    connect.add_argument("--password", "-p", help="Password for basic auth")
    connect.add_argument("--token", "-t", help="Bearer token")
    connect.add_argument("--verbose", "-v", action="store_true", help="Debug logging")

    return parser


def main() -> None:
    """CLI entry point."""
    parser = _build_parser()
    args = parser.parse_args()

    if args.command != "connect":
        parser.print_help()
        sys.exit(1)

    if args.verbose:
        logging.basicConfig(level=logging.DEBUG)
    else:
        logging.basicConfig(level=logging.WARNING)

    conn = WsxConnection(
        url=args.url,
        username=args.user,
        password=args.password,
        token=args.token,
    )
    repl = Repl(conn)

    try:
        asyncio.run(repl.run())
    except KeyboardInterrupt:
        print("\nBye.")


if __name__ == "__main__":
    main()
