from __future__ import annotations

from typing import Annotated

from fastapi import APIRouter, HTTPException, Path, Query, Request, status

from app.dependencies import AuthDep, Fail2BanConfigDirDep, Fail2BanSocketDep
from app.exceptions import (
    ActionAlreadyExistsError,
    ActionNameError,
    ActionNotFoundError,
    ActionReadonlyError,
    ConfigWriteError,
)
from app.models.config import (
    ActionConfig,
    ActionCreateRequest,
    ActionListResponse,
    ActionUpdateRequest,
)
from app.services import action_config_service

router: APIRouter = APIRouter(prefix="/actions", tags=["Action Config"])

_ActionNamePath = Annotated[
    str,
    Path(description='Action base name, e.g. ``iptables`` or ``iptables.conf``.'),
]

_NamePath = Annotated[
    str,
    Path(description='Jail name as configured in fail2ban.'),
]


def _action_not_found(name: str) -> HTTPException:
    return HTTPException(
        status_code=status.HTTP_404_NOT_FOUND,
        detail=f"Action not found: {name!r}",
    )


def _bad_request(message: str) -> HTTPException:
    return HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)


def _not_found(name: str) -> HTTPException:
    return HTTPException(
        status_code=status.HTTP_404_NOT_FOUND,
        detail=f"Jail not found: {name!r}",
    )

@router.get(
    "",
    response_model=ActionListResponse,
    summary="List all available actions with active/inactive status",
)
async def list_actions(
    request: Request,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    socket_path: Fail2BanSocketDep,
) -> ActionListResponse:
    """Return all actions discovered in ``action.d/`` with active/inactive status.

    Scans ``{config_dir}/action.d/`` for ``.conf`` files, merges any
    corresponding ``.local`` overrides, and cross-references each action's
    name against the ``action`` fields of currently running jails to determine
    whether it is active.

    Active actions (those used by at least one running jail) are sorted to the
    top of the list; inactive actions follow.  Both groups are sorted
    alphabetically within themselves.

    Args:
        request: FastAPI request object.
        _auth: Validated session — enforces authentication.

    Returns:
        :class:`~app.models.config.ActionListResponse` with all discovered
        actions.
    """
    result = await action_config_service.list_actions(config_dir, socket_path)
    result.actions.sort(key=lambda a: (not a.active, a.name.lower()))
    return result




@router.get(
    "/{name}",
    response_model=ActionConfig,
    summary="Return full parsed detail for a single action",
)
async def get_action(
    request: Request,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    socket_path: Fail2BanSocketDep,
    name: _ActionNamePath,
) -> ActionConfig:
    """Return the full parsed configuration and active/inactive status for one action.

    Reads ``{config_dir}/action.d/{name}.conf``, merges any corresponding
    ``.local`` override, and annotates the result with ``active``,
    ``used_by_jails``, ``source_file``, and ``has_local_override``.

    Args:
        request: FastAPI request object.
        _auth: Validated session — enforces authentication.
        name: Action base name (with or without ``.conf`` extension).

    Returns:
        :class:`~app.models.config.ActionConfig`.

    Raises:
        HTTPException: 404 if the action is not found in ``action.d/``.
    """
    try:
        return await action_config_service.get_action(config_dir, socket_path, name)
    except ActionNotFoundError:
        raise _action_not_found(name) from None


# ---------------------------------------------------------------------------
# Action write endpoints (Task 3.2)
# ---------------------------------------------------------------------------




@router.put(
    "/{name}",
    response_model=ActionConfig,
    summary="Update an action's .local override with new lifecycle command values",
)
async def update_action(
    request: Request,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    socket_path: Fail2BanSocketDep,
    name: _ActionNamePath,
    body: ActionUpdateRequest,
    reload: bool = Query(default=False, description="Reload fail2ban after writing."),
) -> ActionConfig:
    """Update an action's ``[Definition]`` fields by writing a ``.local`` override.

    Only non-``null`` fields in the request body are written.  The original
    ``.conf`` file is never modified.

    Args:
        request: FastAPI request object.
        _auth: Validated session.
        name: Action base name (with or without ``.conf`` extension).
        body: Partial update — lifecycle commands and ``[Init]`` parameters.
        reload: When ``true``, trigger a fail2ban reload after writing.

    Returns:
        Updated :class:`~app.models.config.ActionConfig`.

    Raises:
        HTTPException: 400 if *name* contains invalid characters.
        HTTPException: 404 if the action does not exist.
        HTTPException: 500 if writing the ``.local`` file fails.
    """
    try:
        return await action_config_service.update_action(config_dir, socket_path, name, body, do_reload=reload)
    except ActionNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ActionNotFoundError:
        raise _action_not_found(name) from None
    except ConfigWriteError as exc:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to write action override: {exc}",
        ) from exc




@router.post(
    "",
    response_model=ActionConfig,
    status_code=status.HTTP_201_CREATED,
    summary="Create a new user-defined action",
)
async def create_action(
    request: Request,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    socket_path: Fail2BanSocketDep,
    body: ActionCreateRequest,
    reload: bool = Query(default=False, description="Reload fail2ban after creating."),
) -> ActionConfig:
    """Create a new user-defined action at ``action.d/{name}.local``.

    Returns 409 if a ``.conf`` or ``.local`` for the requested name already
    exists.

    Args:
        request: FastAPI request object.
        _auth: Validated session.
        body: Action name and ``[Definition]`` lifecycle fields.
        reload: When ``true``, trigger a fail2ban reload after creating.

    Returns:
        :class:`~app.models.config.ActionConfig` for the new action.

    Raises:
        HTTPException: 400 if the name contains invalid characters.
        HTTPException: 409 if the action already exists.
        HTTPException: 500 if writing fails.
    """
    try:
        return await action_config_service.create_action(config_dir, socket_path, body, do_reload=reload)
    except ActionNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ActionAlreadyExistsError as exc:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Action {exc.name!r} already exists.",
        ) from exc
    except ConfigWriteError as exc:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to write action: {exc}",
        ) from exc




@router.delete(
    "/{name}",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Delete a user-created action's .local file",
)
async def delete_action(
    request: Request,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    name: _ActionNamePath,
) -> None:
    """Delete a user-created action's ``.local`` override file.

    Shipped ``.conf``-only actions cannot be deleted (returns 409).  When
    both a ``.conf`` and a ``.local`` exist, only the ``.local`` is removed.

    Args:
        request: FastAPI request object.
        _auth: Validated session.
        name: Action base name.

    Raises:
        HTTPException: 400 if *name* contains invalid characters.
        HTTPException: 404 if the action does not exist.
        HTTPException: 409 if the action is a shipped default (conf-only).
        HTTPException: 500 if deletion fails.
    """
    try:
        await action_config_service.delete_action(config_dir, name)
    except ActionNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ActionNotFoundError:
        raise _action_not_found(name) from None
    except ActionReadonlyError as exc:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=str(exc),
        ) from exc
    except ConfigWriteError as exc:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to delete action: {exc}",
        ) from exc




# ---------------------------------------------------------------------------
# fail2ban log viewer endpoints
# ---------------------------------------------------------------------------




