from __future__ import annotations

from typing import Annotated

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

from app.dependencies import (
    AppDep,
    AuthDep,
    Fail2BanConfigDirDep,
    Fail2BanSocketDep,
    Fail2BanStartCommandDep,
    PendingRecoveryDep,
)
from app.exceptions import (
    ActionNameError,
    ActionNotFoundError,
    ConfigOperationError,
    ConfigValidationError,
    ConfigWriteError,
    Fail2BanConnectionError,
    FilterNameError,
    FilterNotFoundError,
    JailAlreadyActiveError,
    JailAlreadyInactiveError,
    JailNameError,
    JailNotFoundError,
    JailNotFoundInConfigError,
)
from app.models.config import (
    ActivateJailRequest,
    AddLogPathRequest,
    AssignActionRequest,
    AssignFilterRequest,
    InactiveJailListResponse,
    JailActivationResponse,
    JailConfigListResponse,
    JailConfigResponse,
    JailConfigUpdate,
    JailValidationResult,
    PendingRecovery,
    RollbackResponse,
)
from app.services import (
    action_config_service,
    config_service,
    filter_config_service,
    jail_config_service,
)
from app.utils.runtime_state import (
    clear_activation_record,
    clear_pending_recovery,
    record_activation,
)

router: APIRouter = APIRouter(prefix="/jails", tags=["Jail Config"])

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


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


def _bad_gateway(exc: Exception) -> HTTPException:
    return HTTPException(
        status_code=status.HTTP_502_BAD_GATEWAY,
        detail=f"Cannot reach fail2ban: {exc}",
    )


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


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


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


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

@router.get(
    "",
    response_model=JailConfigListResponse,
    summary="List configuration for all active jails",
)
async def get_jail_configs(
    request: Request,
    _auth: AuthDep,
    socket_path: Fail2BanSocketDep,
) -> JailConfigListResponse:
    """Return editable configuration for every active fail2ban jail.

    Fetches ban time, find time, max retries, regex patterns, log paths,
    date pattern, encoding, backend, and attached actions for all jails.

    Args:
        request: Incoming request (used to access ``app.state``).
        _auth: Validated session — enforces authentication.

    Returns:
        :class:`~app.models.config.JailConfigListResponse`.
    """
    try:
        return await config_service.list_jail_configs(socket_path)
    except Fail2BanConnectionError as exc:
        raise _bad_gateway(exc) from exc




@router.get(
    "/inactive",
    response_model=InactiveJailListResponse,
    summary="List all inactive jails discovered in config files",
)
async def get_inactive_jails(
    request: Request,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    socket_path: Fail2BanSocketDep,
) -> InactiveJailListResponse:
    """Return all jails defined in fail2ban config files that are not running.

    Parses ``jail.conf``, ``jail.local``, and ``jail.d/`` following the
    fail2ban merge order.  Jails that fail2ban currently reports as running
    are excluded; only truly inactive entries are returned.

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

    Returns:
        :class:`~app.models.config.InactiveJailListResponse`.
    """
    return await jail_config_service.list_inactive_jails(config_dir, socket_path)


@router.get(
    "/pending-recovery",
    response_model=PendingRecovery | None,
    summary="Return active crash-recovery record if one exists",
)
async def get_pending_recovery(
    _auth: AuthDep,
    pending_recovery: PendingRecoveryDep,
) -> PendingRecovery | None:
    """Return the current :class:`~app.models.config.PendingRecovery` record.

    A non-null response means fail2ban crashed shortly after a jail activation
    and the user should be offered a rollback option.  Returns ``null`` (HTTP
    200 with ``null`` body) when no recovery is pending.

    Args:
        request: FastAPI request object.
        _auth: Validated session.

    Returns:
        :class:`~app.models.config.PendingRecovery` or ``None``.
    """
    return pending_recovery


@router.get(
    "/{name}",
    response_model=JailConfigResponse,
    summary="Return configuration for a single jail",
)
async def get_jail_config(
    request: Request,
    _auth: AuthDep,
    socket_path: Fail2BanSocketDep,
    name: _NamePath,
) -> JailConfigResponse:
    """Return the full editable configuration for one fail2ban jail.

    Args:
        request: Incoming request.
        _auth: Validated session.
        name: Jail name.

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

    Raises:
        HTTPException: 404 when the jail does not exist.
        HTTPException: 502 when fail2ban is unreachable.
    """
    try:
        return await config_service.get_jail_config(socket_path, name)
    except JailNotFoundError:
        raise _not_found(name) from None
    except Fail2BanConnectionError as exc:
        raise _bad_gateway(exc) from exc




@router.put(
    "/{name}",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Update jail configuration",
)
async def update_jail_config(
    request: Request,
    _auth: AuthDep,
    socket_path: Fail2BanSocketDep,
    name: _NamePath,
    body: JailConfigUpdate,
) -> None:
    """Update one or more configuration fields for an active fail2ban jail.

    Regex patterns are validated before being sent to fail2ban.  An invalid
    pattern returns 422 with the regex error message.

    Args:
        request: Incoming request.
        _auth: Validated session.
        name: Jail name.
        body: Partial update — only non-None fields are written.

    Raises:
        HTTPException: 404 when the jail does not exist.
        HTTPException: 422 when a regex pattern fails to compile.
        HTTPException: 400 when a set command is rejected.
        HTTPException: 502 when fail2ban is unreachable.
    """
    try:
        await config_service.update_jail_config(socket_path, name, body)
    except JailNotFoundError:
        raise _not_found(name) from None
    except ConfigValidationError as exc:
        raise _unprocessable(str(exc)) from exc
    except ConfigOperationError as exc:
        raise _bad_request(str(exc)) from exc
    except Fail2BanConnectionError as exc:
        raise _bad_gateway(exc) from exc


# ---------------------------------------------------------------------------
# Global configuration endpoints
# ---------------------------------------------------------------------------




@router.post(
    "/{name}/logpath",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Add a log file path to an existing jail",
)
async def add_log_path(
    request: Request,
    _auth: AuthDep,
    socket_path: Fail2BanSocketDep,
    name: _NamePath,
    body: AddLogPathRequest,
) -> None:
    """Register an additional log file for an existing jail to monitor.

    Uses ``set <jail> addlogpath <path> <tail|head>`` to add the path
    without requiring a daemon restart.

    Args:
        request: Incoming request.
        _auth: Validated session.
        name: Jail name.
        body: Log path and tail/head preference.

    Raises:
        HTTPException: 404 when the jail does not exist.
        HTTPException: 400 when the command is rejected.
        HTTPException: 502 when fail2ban is unreachable.
    """
    try:
        await config_service.add_log_path(socket_path, name, body)
    except JailNotFoundError:
        raise _not_found(name) from None
    except ConfigOperationError as exc:
        raise _bad_request(str(exc)) from exc
    except Fail2BanConnectionError as exc:
        raise _bad_gateway(exc) from exc




@router.delete(
    "/{name}/logpath",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Remove a monitored log path from a jail",
)
async def delete_log_path(
    request: Request,
    _auth: AuthDep,
    socket_path: Fail2BanSocketDep,
    name: _NamePath,
    log_path: str = Query(..., description="Absolute path of the log file to stop monitoring."),
) -> None:
    """Stop a jail from monitoring the specified log file.

    Uses ``set <jail> dellogpath <path>`` to remove the log path at runtime
    without requiring a daemon restart.

    Args:
        request: Incoming request.
        _auth: Validated session.
        name: Jail name.
        log_path: Absolute path to the log file to remove (query parameter).

    Raises:
        HTTPException: 404 when the jail does not exist.
        HTTPException: 400 when the command is rejected.
        HTTPException: 502 when fail2ban is unreachable.
    """
    try:
        await config_service.delete_log_path(socket_path, name, log_path)
    except JailNotFoundError:
        raise _not_found(name) from None
    except ConfigOperationError as exc:
        raise _bad_request(str(exc)) from exc
    except Fail2BanConnectionError as exc:
        raise _bad_gateway(exc) from exc




@router.post(
    "/{name}/activate",
    response_model=JailActivationResponse,
    summary="Activate an inactive jail",
)
async def activate_jail(
    app: AppDep,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    socket_path: Fail2BanSocketDep,
    name: _NamePath,
    body: ActivateJailRequest | None = None,
) -> JailActivationResponse:
    """Enable an inactive jail and reload fail2ban.

    Writes ``enabled = true`` (plus any override values from the request
    body) to ``jail.d/{name}.local`` and triggers a full fail2ban reload so
    the jail starts immediately.

    Args:
        app: FastAPI application instance.
        _auth: Validated session.
        name: Name of the jail to activate.
        body: Optional override values (bantime, findtime, maxretry, port,
              logpath).

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

    Raises:
        HTTPException: 400 if *name* contains invalid characters.
        HTTPException: 404 if *name* is not found in any config file.
        HTTPException: 409 if the jail is already active.
        HTTPException: 502 if fail2ban is unreachable.
    """
    req = body if body is not None else ActivateJailRequest()

    try:
        result = await jail_config_service.activate_jail(config_dir, socket_path, name, req)
    except JailNameError as exc:
        raise _bad_request(str(exc)) from exc
    except JailNotFoundInConfigError:
        raise _not_found(name) from None
    except JailAlreadyActiveError:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Jail {name!r} is already active.",
        ) from None
    except ConfigWriteError as exc:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to write config override: {exc}",
        ) from exc
    except Fail2BanConnectionError as exc:
        raise _bad_gateway(exc) from exc

    if result.active:
        record_activation(app, name)

    return result




@router.post(
    "/{name}/deactivate",
    response_model=JailActivationResponse,
    summary="Deactivate an active jail",
)
async def deactivate_jail(
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    socket_path: Fail2BanSocketDep,
    name: _NamePath,
) -> JailActivationResponse:
    """Disable an active jail and reload fail2ban.

    Writes ``enabled = false`` to ``jail.d/{name}.local`` and triggers a
    full fail2ban reload so the jail stops immediately.

    Args:
        _auth: Validated session.
        name: Name of the jail to deactivate.

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

    Raises:
        HTTPException: 400 if *name* contains invalid characters.
        HTTPException: 404 if *name* is not found in any config file.
        HTTPException: 409 if the jail is already inactive.
        HTTPException: 502 if fail2ban is unreachable.
    """

    try:
        result = await jail_config_service.deactivate_jail(config_dir, socket_path, name)
    except JailNameError as exc:
        raise _bad_request(str(exc)) from exc
    except JailNotFoundInConfigError:
        raise _not_found(name) from None
    except JailAlreadyInactiveError:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Jail {name!r} is already inactive.",
        ) from None
    except ConfigWriteError as exc:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to write config override: {exc}",
        ) from exc
    except Fail2BanConnectionError as exc:
        raise _bad_gateway(exc) from exc

    return result




@router.delete(
    "/{name}/local",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Delete the jail.d override file for an inactive jail",
)
async def delete_jail_local_override(
    request: Request,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    socket_path: Fail2BanSocketDep,
    name: _NamePath,
) -> None:
    """Remove the ``jail.d/{name}.local`` override file for an inactive jail.

    This endpoint is the clean-up action for inactive jails that still carry
    a ``.local`` override file (e.g. one written with ``enabled = false`` by a
    previous deactivation).  The file is deleted without modifying fail2ban's
    running state, since the jail is already inactive.

    Args:
        request: FastAPI request object.
        _auth: Validated session.
        name: Name of the jail whose ``.local`` file should be removed.

    Raises:
        HTTPException: 400 if *name* contains invalid characters.
        HTTPException: 404 if *name* is not found in any config file.
        HTTPException: 409 if the jail is currently active.
        HTTPException: 500 if the file cannot be deleted.
        HTTPException: 502 if fail2ban is unreachable.
    """

    try:
        await jail_config_service.delete_jail_local_override(config_dir, socket_path, name)
    except JailNameError as exc:
        raise _bad_request(str(exc)) from exc
    except JailNotFoundInConfigError:
        raise _not_found(name) from None
    except JailAlreadyActiveError:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Jail {name!r} is currently active; deactivate it first.",
        ) from None
    except ConfigWriteError as exc:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to delete config override: {exc}",
        ) from exc
    except Fail2BanConnectionError as exc:
        raise _bad_gateway(exc) from exc


# ---------------------------------------------------------------------------
# Jail validation & rollback endpoints (Task 3)
# ---------------------------------------------------------------------------




@router.post(
    "/{name}/validate",
    response_model=JailValidationResult,
    summary="Validate jail configuration before activation",
)
async def validate_jail(
    request: Request,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    name: _NamePath,
) -> JailValidationResult:
    """Run pre-activation validation checks on a jail configuration.

    Validates filter and action file existence, regex pattern compilation, and
    log path existence without modifying any files or reloading fail2ban.

    Args:
        request: FastAPI request object.
        _auth: Validated session.
        name: Jail name to validate.

    Returns:
        :class:`~app.models.config.JailValidationResult` with any issues found.

    Raises:
        HTTPException: 400 if *name* contains invalid characters.
        HTTPException: 404 if *name* is not found in any config file.
    """
    try:
        return await jail_config_service.validate_jail_config(config_dir, name)
    except JailNameError as exc:
        raise _bad_request(str(exc)) from exc


@router.post(
    "/{name}/rollback",
    response_model=RollbackResponse,
    summary="Disable a bad jail config and restart fail2ban",
)
async def rollback_jail(
    _auth: AuthDep,
    app: AppDep,
    config_dir: Fail2BanConfigDirDep,
    socket_path: Fail2BanSocketDep,
    start_cmd: Fail2BanStartCommandDep,
    name: _NamePath,
) -> RollbackResponse:
    """Disable the specified jail and attempt to restart fail2ban.

    Writes ``enabled = false`` to ``jail.d/{name}.local`` (works even when
    fail2ban is down — no socket is needed), then runs the configured start
    command and waits up to ten seconds for the daemon to come back online.

    On success, clears the :class:`~app.models.config.PendingRecovery` record.

    Args:
        _auth: Validated session.
        app: FastAPI application instance.
        name: Jail name to disable and roll back.

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

    Raises:
        HTTPException: 400 if *name* contains invalid characters.
        HTTPException: 500 if writing the .local override file fails.
    """
    start_cmd_parts: list[str] = start_cmd.split()

    try:
        result = await jail_config_service.rollback_jail(config_dir, socket_path, name, start_cmd_parts)
    except JailNameError as exc:
        raise _bad_request(str(exc)) from exc
    except ConfigWriteError as exc:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to write config override: {exc}",
        ) from exc

    if result.fail2ban_running:
        clear_pending_recovery(app)
        clear_activation_record(app)

    return result


@router.post(
    "/{name}/filter",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Assign a filter to a jail",
)
async def assign_filter_to_jail(
    request: Request,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    socket_path: Fail2BanSocketDep,
    name: _NamePath,
    body: AssignFilterRequest,
    reload: bool = Query(default=False, description="Reload fail2ban after assigning."),
) -> None:
    """Write ``filter = {filter_name}`` to the jail's ``.local`` config.

    Existing keys in the jail's ``.local`` file are preserved.  If the file
    does not exist it is created.

    Args:
        request: FastAPI request object.
        _auth: Validated session.
        name: Jail name.
        body: Filter to assign.
        reload: When ``true``, trigger a fail2ban reload after writing.

    Raises:
        HTTPException: 400 if *name* or *filter_name* contain invalid characters.
        HTTPException: 404 if the jail or filter does not exist.
        HTTPException: 500 if writing fails.
    """
    try:
        await filter_config_service.assign_filter_to_jail(config_dir, socket_path, name, body, do_reload=reload)
    except (JailNameError, FilterNameError) as exc:
        raise _bad_request(str(exc)) from exc
    except JailNotFoundInConfigError:
        raise _not_found(name) from None
    except FilterNotFoundError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Filter not found: {exc.name!r}",
        ) from exc
    except ConfigWriteError as exc:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to write jail override: {exc}",
        ) from exc


@router.post(
    "/{name}/action",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Add an action to a jail",
)
async def assign_action_to_jail(
    request: Request,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    socket_path: Fail2BanSocketDep,
    name: _NamePath,
    body: AssignActionRequest,
    reload: bool = Query(default=False, description="Reload fail2ban after assigning."),
) -> None:
    """Append an action entry to the jail's ``.local`` config.

    Existing keys in the jail's ``.local`` file are preserved.  If the file
    does not exist it is created.  The action is not duplicated if it is
    already present.

    Args:
        request: FastAPI request object.
        _auth: Validated session.
        name: Jail name.
        body: Action to add plus optional per-jail parameters.
        reload: When ``true``, trigger a fail2ban reload after writing.

    Raises:
        HTTPException: 400 if *name* or *action_name* contain invalid characters.
        HTTPException: 404 if the jail or action does not exist.
        HTTPException: 500 if writing fails.
    """
    try:
        await action_config_service.assign_action_to_jail(config_dir, socket_path, name, body, do_reload=reload)
    except (JailNameError, ActionNameError) as exc:
        raise _bad_request(str(exc)) from exc
    except JailNotFoundInConfigError:
        raise _not_found(name) from None
    except ActionNotFoundError as exc:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Action not found: {exc.name!r}",
        ) from exc
    except ConfigWriteError as exc:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to write jail override: {exc}",
        ) from exc


@router.delete(
    "/{name}/action/{action_name}",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Remove an action from a jail",
)
async def remove_action_from_jail(
    request: Request,
    _auth: AuthDep,
    config_dir: Fail2BanConfigDirDep,
    socket_path: Fail2BanSocketDep,
    name: _NamePath,
    action_name: Annotated[str, Path(description="Action base name to remove.")],
    reload: bool = Query(default=False, description="Reload fail2ban after removing."),
) -> None:
    """Remove an action from the jail's ``.local`` config.

    If the jail has no ``.local`` file or the action is not listed there,
    the call is silently idempotent.

    Args:
        request: FastAPI request object.
        _auth: Validated session.
        name: Jail name.
        action_name: Base name of the action to remove.
        reload: When ``true``, trigger a fail2ban reload after writing.

    Raises:
        HTTPException: 400 if *name* or *action_name* contain invalid characters.
        HTTPException: 404 if the jail is not found in config files.
        HTTPException: 500 if writing fails.
    """
    try:
        await action_config_service.remove_action_from_jail(
            config_dir,
            socket_path,
            name,
            action_name,
            do_reload=reload,
        )
    except (JailNameError, ActionNameError) as exc:
        raise _bad_request(str(exc)) from exc
    except JailNotFoundInConfigError:
        raise _not_found(name) from None
    except ConfigWriteError as exc:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to write jail override: {exc}",
        ) from exc
# ---------------------------------------------------------------------------
# Filter discovery endpoints (Task 2.1)
# ---------------------------------------------------------------------------




