import logging
from typing import Any, Dict, List

from fastapi import APIRouter, Body, Depends, Security
from fastapi_pagination import (
    Page,
    Params,
)
from fastapi_pagination.bases import AbstractPage
from fastapi_pagination.ext.sqlalchemy import paginate

from fidesops.schemas.shared_schemas import FidesOpsKey
from pydantic import conlist
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from starlette.exceptions import HTTPException
from starlette.status import HTTP_404_NOT_FOUND

from fidesops.api import deps
from fidesops.api.v1 import scope_registry as scopes
from fidesops.api.v1 import urn_registry as urls
from fidesops.common_exceptions import (
    DataCategoryNotSupported,
    PolicyValidationError,
    RuleValidationError,
    RuleTargetValidationError,
    KeyOrNameAlreadyExists,
)
from fidesops.models.client import ClientDetail
from fidesops.models.policy import (
    ActionType,
    Policy,
    Rule,
    RuleTarget,
)
from fidesops.models.storage import StorageConfig
from fidesops.schemas import policy as schemas
from fidesops.schemas.api import BulkUpdateFailed
from fidesops.util.oauth_util import verify_oauth_client


router = APIRouter(tags=["Policy"], prefix=urls.V1_URL_PREFIX)

logger = logging.getLogger(__name__)


@router.get(
    urls.POLICY_LIST,
    status_code=200,
    response_model=Page[schemas.PolicyResponse],
    dependencies=[Security(verify_oauth_client, scopes=[scopes.POLICY_READ])],
)
def get_policy_list(
    *,
    db: Session = Depends(deps.get_db),
    params: Params = Depends(),
) -> AbstractPage[Policy]:
    """
    Return a paginated list of all Policy records in this system
    """
    logger.info(f"Finding all policies with pagination params '{params}'")
    policies = Policy.query(db=db)
    return paginate(policies, params=params)


def get_policy_or_error(db: Session, policy_key: FidesOpsKey) -> Policy:
    """Helper method to load Policy or throw a 404"""
    logger.info(f"Finding policy with key '{policy_key}'")
    policy = Policy.get_by(db=db, field="key", value=policy_key)
    if not policy:
        raise HTTPException(
            status_code=HTTP_404_NOT_FOUND,
            detail=f"No Policy found for key {policy_key}.",
        )

    return policy


@router.get(
    urls.POLICY_DETAIL,
    status_code=200,
    response_model=schemas.PolicyResponse,
    dependencies=[Security(verify_oauth_client, scopes=[scopes.POLICY_READ])],
)
def get_policy(
    *,
    policy_key: FidesOpsKey,
    db: Session = Depends(deps.get_db),
) -> schemas.PolicyResponse:
    """
    Return a single Policy
    """
    return get_policy_or_error(db, policy_key)


@router.patch(
    urls.POLICY_LIST,
    status_code=200,
    response_model=schemas.BulkPutPolicyResponse,
)
def create_or_update_policies(
    *,
    client: ClientDetail = Security(
        verify_oauth_client,
        scopes=[scopes.POLICY_CREATE_OR_UPDATE],
    ),
    db: Session = Depends(deps.get_db),
    data: conlist(schemas.Policy, max_items=50) = Body(...),  # type: ignore
) -> schemas.BulkPutPolicyResponse:
    """
    Given a list of policy data elements, create or update corresponding Policy objects
    or report failure
    """
    created_or_updated: List[Policy] = []
    failed: List[BulkUpdateFailed] = []
    logger.info(f"Starting bulk upsert for {len(data)} policies")

    for policy_schema in data:
        policy_data: Dict[str, Any] = dict(policy_schema)
        try:
            policy = Policy.create_or_update(
                db=db,
                data={
                    "name": policy_data["name"],
                    "key": policy_data.get("key"),
                    "client_id": client.id,
                },
            )
        except KeyOrNameAlreadyExists as exc:
            logger.warning("Create/update failed for policy: %s", exc)
            failure = {
                "message": exc.args[0],
                "data": policy_data,
            }
            failed.append(BulkUpdateFailed(**failure))
            continue
        except PolicyValidationError as exc:
            logger.warning("Create/update failed for policy: %s", exc)
            failure = {
                "message": "This record could not be added because the data provided was invalid.",
                "data": policy_data,
            }
            failed.append(BulkUpdateFailed(**failure))
            continue
        else:
            created_or_updated.append(policy)

    return schemas.BulkPutPolicyResponse(
        succeeded=created_or_updated,
        failed=failed,
    )


@router.patch(
    urls.RULE_LIST,
    status_code=200,
    response_model=schemas.BulkPutRuleResponse,
)
def create_or_update_rules(
    *,
    client: ClientDetail = Security(
        verify_oauth_client,
        scopes=[scopes.RULE_CREATE_OR_UPDATE],
    ),
    policy_key: FidesOpsKey,
    db: Session = Depends(deps.get_db),
    input_data: conlist(schemas.RuleCreate, max_items=50) = Body(...),  # type: ignore
) -> schemas.BulkPutRuleResponse:
    """
    Given a list of Rule data elements, create or update corresponding Rule objects
    or report failure
    """
    logger.info(f"Finding policy with key '{policy_key}'")

    policy = get_policy_or_error(db, policy_key)

    created_or_updated: List[Rule] = []
    failed: List[BulkUpdateFailed] = []

    logger.info(
        f"Starting bulk upsert for {len(input_data)} rules on policy {policy_key}"
    )

    for schema in input_data:
        # Validate all FKs in the input data exist
        associated_storage_config_id = None
        if schema.action_type == ActionType.access.value:
            # Only validate the associated StorageConfig on access rules
            storage_destination_key = schema.storage_destination_key
            associated_storage_config: StorageConfig = StorageConfig.get_by(
                db=db,
                field="key",
                value=storage_destination_key,
            )
            if not associated_storage_config:
                logger.warning(
                    f"No storage config found with key {storage_destination_key}"
                )
                failure = {
                    "message": f"A StorageConfig with key {storage_destination_key} does not exist",
                    "data": dict(
                        schema
                    ),  # Be sure to pass the schema out the same way it came in
                }
                failed.append(BulkUpdateFailed(**failure))
                continue
            else:
                associated_storage_config_id = associated_storage_config.id

        masking_strategy_data = None
        if schema.masking_strategy:
            masking_strategy_data = schema.masking_strategy.dict()

        try:
            rule = Rule.create_or_update(
                db=db,
                data={
                    "action_type": schema.action_type,
                    "client_id": client.id,
                    "key": schema.key,
                    "name": schema.name,
                    "policy_id": policy.id,
                    "storage_destination_id": associated_storage_config_id,
                    "masking_strategy": masking_strategy_data,
                },
            )
        except KeyOrNameAlreadyExists as exc:
            logger.warning(
                f"Create/update failed for rule '{schema.key}' on policy {policy_key}: {exc}"
            )
            failure = {
                "message": exc.args[0],
                "data": dict(schema),
            }
            failed.append(BulkUpdateFailed(**failure))
            continue
        except RuleValidationError as exc:
            logger.warning(
                f"Create/update failed for rule '{schema.key}' on policy {policy_key}: {exc}"
            )
            failure = {
                "message": exc.args[0],
                "data": dict(schema),
            }
            failed.append(BulkUpdateFailed(**failure))
            continue
        except ValueError as exc:
            logger.warning(
                f"Create/update failed for rule '{schema.key}' on policy {policy_key}: {exc}"
            )
            failure = {
                "message": exc.args[0],
                "data": dict(schema),
            }
            failed.append(BulkUpdateFailed(**failure))
            continue
        else:
            created_or_updated.append(rule)

    return schemas.BulkPutRuleResponse(succeeded=created_or_updated, failed=failed)


@router.delete(
    urls.RULE_DETAIL,
    status_code=204,
    dependencies=[Security(verify_oauth_client, scopes=[scopes.RULE_DELETE])],
)
def delete_rule(
    *,
    policy_key: FidesOpsKey,
    rule_key: FidesOpsKey,
    db: Session = Depends(deps.get_db),
) -> None:
    """
    Delete a policy rule.
    """
    policy = get_policy_or_error(db, policy_key)

    logger.info(f"Finding rule with key '{rule_key}'")

    rule = Rule.filter(
        db=db, conditions=(Rule.key == rule_key and Rule.policy_id == policy.id)
    ).first()
    if not rule:
        raise HTTPException(
            status_code=HTTP_404_NOT_FOUND,
            detail=f"No Rule found for key {rule_key} on Policy {policy_key}.",
        )

    logger.info(f"Deleting rule with key '{rule_key}'")
    rule.delete(db=db)


@router.patch(
    urls.RULE_TARGET_LIST,
    status_code=200,
    response_model=schemas.BulkPutRuleTargetResponse,
)
def create_or_update_rule_targets(
    *,
    client: ClientDetail = Security(
        verify_oauth_client, scopes=[scopes.RULE_CREATE_OR_UPDATE]
    ),
    policy_key: FidesOpsKey,
    rule_key: FidesOpsKey,
    db: Session = Depends(deps.get_db),
    input_data: conlist(schemas.RuleTarget, max_items=50) = Body(...),  # type: ignore
) -> schemas.BulkPutRuleTargetResponse:
    """
    Given a list of Rule data elements, create corresponding Rule objects
    or report failure
    """
    policy = get_policy_or_error(db, policy_key)

    logger.info(f"Finding rule with key '{rule_key}'")
    rule = Rule.filter(
        db=db, conditions=(Rule.key == rule_key and Rule.policy_id == policy.id)
    ).first()
    if not rule:
        raise HTTPException(
            status_code=HTTP_404_NOT_FOUND,
            detail=f"No Rule found for key {rule_key} on Policy {policy_key}.",
        )

    created_or_updated = []
    failed = []
    logger.info(
        f"Starting bulk upsert for {len(input_data)} rule targets on rule {rule_key}"
    )
    for schema in input_data:
        try:
            target = RuleTarget.create_or_update(
                db=db,
                data={
                    "name": schema.name,
                    "key": schema.key,
                    "data_category": schema.data_category,
                    "rule_id": rule.id,
                    "client_id": client.id,
                },
            )
        except KeyOrNameAlreadyExists as exc:
            logger.warning(
                f"Create/update failed for rule target {schema.key} on rule {rule_key}: {exc}"
            )
            failure = {
                "message": exc.args[0],
                "data": dict(schema),
            }
            failed.append(BulkUpdateFailed(**failure))
            continue
        except (
            DataCategoryNotSupported,
            PolicyValidationError,
            RuleTargetValidationError,
        ) as exc:
            logger.warning(
                f"Create/update failed for rule target {schema.key} on rule {rule_key}: {exc}"
            )
            failure = {
                "message": exc.args[0],
                "data": dict(schema),
            }
            failed.append(BulkUpdateFailed(**failure))
            continue
        except IntegrityError as exc:
            logger.warning(
                f"Create/update failed for rule target {schema.key} on rule {rule_key}: {exc}"
            )
            failure = {
                "message": f"DataCategory {schema.data_category} is already specified on Rule with ID {rule.id}",
                "data": dict(schema),
            }
            failed.append(BulkUpdateFailed(**failure))
        else:
            created_or_updated.append(target)

    return schemas.BulkPutRuleTargetResponse(
        succeeded=created_or_updated,
        failed=failed,
    )


@router.delete(
    urls.RULE_TARGET_DETAIL,
    status_code=204,
    dependencies=[Security(verify_oauth_client, scopes=[scopes.RULE_DELETE])],
)
def delete_rule_target(
    *,
    policy_key: FidesOpsKey,
    rule_key: FidesOpsKey,
    rule_target_key: FidesOpsKey,
    db: Session = Depends(deps.get_db),
) -> None:
    """
    Delete the rule target.
    """
    policy = get_policy_or_error(db, policy_key)

    logger.info(f"Finding rule with key '{rule_key}'")
    rule = Rule.filter(
        db=db, conditions=(Rule.key == rule_key and Rule.policy_id == policy.id)
    ).first()
    if not rule:
        raise HTTPException(
            status_code=HTTP_404_NOT_FOUND,
            detail=f"No Rule found for key {rule_key} on Policy {policy_key}.",
        )

    logger.info(f"Finding rule target with key '{rule_target_key}'")
    target = RuleTarget.filter(
        db=db,
        conditions=(
            RuleTarget.key == rule_target_key and RuleTarget.rule_id == rule.id
        ),
    ).first()
    if not target:
        raise HTTPException(
            status_code=HTTP_404_NOT_FOUND,
            detail=f"No RuleTarget found for key {rule_target_key} at Rule {rule_key} on Policy {policy_key}.",
        )

    logger.info(f"Deleting rule target with key '{rule_target_key}'")

    target.delete(db=db)

[(42, 'fastapi.APIRouter', 'APIRouter', ([], {'tags': "['Policy']", 'prefix': 'urls.V1_URL_PREFIX'}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (44, 'logging.getLogger', 'logging.getLogger', (['__name__'], {}), False, 'import logging\n'), (55, 'fastapi.Depends', 'Depends', (['deps.get_db'], {}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (56, 'fastapi.Depends', 'Depends', ([], {}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (62, 'fidesops.models.policy.Policy.query', 'Policy.query', ([], {'db': 'db'}), False, 'from fidesops.models.policy import ActionType, Policy, Rule, RuleTarget\n'), (63, 'fastapi_pagination.ext.sqlalchemy.paginate', 'paginate', (['policies'], {'params': 'params'}), False, 'from fastapi_pagination.ext.sqlalchemy import paginate\n'), (69, 'fidesops.models.policy.Policy.get_by', 'Policy.get_by', ([], {'db': 'db', 'field': '"""key"""', 'value': 'policy_key'}), False, 'from fidesops.models.policy import ActionType, Policy, Rule, RuleTarget\n'), (88, 'fastapi.Depends', 'Depends', (['deps.get_db'], {}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (103, 'fastapi.Security', 'Security', (['verify_oauth_client'], {'scopes': '[scopes.POLICY_CREATE_OR_UPDATE]'}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (107, 'fastapi.Depends', 'Depends', (['deps.get_db'], {}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (108, 'fastapi.Body', 'Body', (['...'], {}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (148, 'fidesops.schemas.policy.BulkPutPolicyResponse', 'schemas.BulkPutPolicyResponse', ([], {'succeeded': 'created_or_updated', 'failed': 'failed'}), True, 'from fidesops.schemas import policy as schemas\n'), (161, 'fastapi.Security', 'Security', (['verify_oauth_client'], {'scopes': '[scopes.RULE_CREATE_OR_UPDATE]'}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (166, 'fastapi.Depends', 'Depends', (['deps.get_db'], {}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (167, 'fastapi.Body', 'Body', (['...'], {}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (260, 'fidesops.schemas.policy.BulkPutRuleResponse', 'schemas.BulkPutRuleResponse', ([], {'succeeded': 'created_or_updated', 'failed': 'failed'}), True, 'from fidesops.schemas import policy as schemas\n'), (272, 'fastapi.Depends', 'Depends', (['deps.get_db'], {}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (301, 'fastapi.Security', 'Security', (['verify_oauth_client'], {'scopes': '[scopes.RULE_CREATE_OR_UPDATE]'}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (306, 'fastapi.Depends', 'Depends', (['deps.get_db'], {}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (307, 'fastapi.Body', 'Body', (['...'], {}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (378, 'fidesops.schemas.policy.BulkPutRuleTargetResponse', 'schemas.BulkPutRuleTargetResponse', ([], {'succeeded': 'created_or_updated', 'failed': 'failed'}), True, 'from fidesops.schemas import policy as schemas\n'), (394, 'fastapi.Depends', 'Depends', (['deps.get_db'], {}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (71, 'starlette.exceptions.HTTPException', 'HTTPException', ([], {'status_code': 'HTTP_404_NOT_FOUND', 'detail': 'f"""No Policy found for key {policy_key}."""'}), False, 'from starlette.exceptions import HTTPException\n'), (108, 'pydantic.conlist', 'conlist', (['schemas.Policy'], {'max_items': '(50)'}), False, 'from pydantic import conlist\n'), (167, 'pydantic.conlist', 'conlist', (['schemas.RuleCreate'], {'max_items': '(50)'}), False, 'from pydantic import conlist\n'), (285, 'starlette.exceptions.HTTPException', 'HTTPException', ([], {'status_code': 'HTTP_404_NOT_FOUND', 'detail': 'f"""No Rule found for key {rule_key} on Policy {policy_key}."""'}), False, 'from starlette.exceptions import HTTPException\n'), (307, 'pydantic.conlist', 'conlist', (['schemas.RuleTarget'], {'max_items': '(50)'}), False, 'from pydantic import conlist\n'), (320, 'starlette.exceptions.HTTPException', 'HTTPException', ([], {'status_code': 'HTTP_404_NOT_FOUND', 'detail': 'f"""No Rule found for key {rule_key} on Policy {policy_key}."""'}), False, 'from starlette.exceptions import HTTPException\n'), (406, 'starlette.exceptions.HTTPException', 'HTTPException', ([], {'status_code': 'HTTP_404_NOT_FOUND', 'detail': 'f"""No Rule found for key {rule_key} on Policy {policy_key}."""'}), False, 'from starlette.exceptions import HTTPException\n'), (419, 'starlette.exceptions.HTTPException', 'HTTPException', ([], {'status_code': 'HTTP_404_NOT_FOUND', 'detail': 'f"""No RuleTarget found for key {rule_target_key} at Rule {rule_key} on Policy {policy_key}."""'}), False, 'from starlette.exceptions import HTTPException\n'), (51, 'fastapi.Security', 'Security', (['verify_oauth_client'], {'scopes': '[scopes.POLICY_READ]'}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (83, 'fastapi.Security', 'Security', (['verify_oauth_client'], {'scopes': '[scopes.POLICY_READ]'}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (190, 'fidesops.models.storage.StorageConfig.get_by', 'StorageConfig.get_by', ([], {'db': 'db', 'field': '"""key"""', 'value': 'storage_destination_key'}), False, 'from fidesops.models.storage import StorageConfig\n'), (215, 'fidesops.models.policy.Rule.create_or_update', 'Rule.create_or_update', ([], {'db': 'db', 'data': "{'action_type': schema.action_type, 'client_id': client.id, 'key': schema.\n    key, 'name': schema.name, 'policy_id': policy.id,\n    'storage_destination_id': associated_storage_config_id,\n    'masking_strategy': masking_strategy_data}"}), False, 'from fidesops.models.policy import ActionType, Policy, Rule, RuleTarget\n'), (281, 'fidesops.models.policy.Rule.filter', 'Rule.filter', ([], {'db': 'db', 'conditions': '(Rule.key == rule_key and Rule.policy_id == policy.id)'}), False, 'from fidesops.models.policy import ActionType, Policy, Rule, RuleTarget\n'), (266, 'fastapi.Security', 'Security', (['verify_oauth_client'], {'scopes': '[scopes.RULE_DELETE]'}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (316, 'fidesops.models.policy.Rule.filter', 'Rule.filter', ([], {'db': 'db', 'conditions': '(Rule.key == rule_key and Rule.policy_id == policy.id)'}), False, 'from fidesops.models.policy import ActionType, Policy, Rule, RuleTarget\n'), (332, 'fidesops.models.policy.RuleTarget.create_or_update', 'RuleTarget.create_or_update', ([], {'db': 'db', 'data': "{'name': schema.name, 'key': schema.key, 'data_category': schema.\n    data_category, 'rule_id': rule.id, 'client_id': client.id}"}), False, 'from fidesops.models.policy import ActionType, Policy, Rule, RuleTarget\n'), (402, 'fidesops.models.policy.Rule.filter', 'Rule.filter', ([], {'db': 'db', 'conditions': '(Rule.key == rule_key and Rule.policy_id == policy.id)'}), False, 'from fidesops.models.policy import ActionType, Policy, Rule, RuleTarget\n'), (412, 'fidesops.models.policy.RuleTarget.filter', 'RuleTarget.filter', ([], {'db': 'db', 'conditions': '(RuleTarget.key == rule_target_key and RuleTarget.rule_id == rule.id)'}), False, 'from fidesops.models.policy import ActionType, Policy, Rule, RuleTarget\n'), (387, 'fastapi.Security', 'Security', (['verify_oauth_client'], {'scopes': '[scopes.RULE_DELETE]'}), False, 'from fastapi import APIRouter, Body, Depends, Security\n'), (135, 'fidesops.schemas.api.BulkUpdateFailed', 'BulkUpdateFailed', ([], {}), False, 'from fidesops.schemas.api import BulkUpdateFailed\n'), (143, 'fidesops.schemas.api.BulkUpdateFailed', 'BulkUpdateFailed', ([], {}), False, 'from fidesops.schemas.api import BulkUpdateFailed\n'), (205, 'fidesops.schemas.api.BulkUpdateFailed', 'BulkUpdateFailed', ([], {}), False, 'from fidesops.schemas.api import BulkUpdateFailed\n'), (235, 'fidesops.schemas.api.BulkUpdateFailed', 'BulkUpdateFailed', ([], {}), False, 'from fidesops.schemas.api import BulkUpdateFailed\n'), (245, 'fidesops.schemas.api.BulkUpdateFailed', 'BulkUpdateFailed', ([], {}), False, 'from fidesops.schemas.api import BulkUpdateFailed\n'), (255, 'fidesops.schemas.api.BulkUpdateFailed', 'BulkUpdateFailed', ([], {}), False, 'from fidesops.schemas.api import BulkUpdateFailed\n'), (350, 'fidesops.schemas.api.BulkUpdateFailed', 'BulkUpdateFailed', ([], {}), False, 'from fidesops.schemas.api import BulkUpdateFailed\n'), (364, 'fidesops.schemas.api.BulkUpdateFailed', 'BulkUpdateFailed', ([], {}), False, 'from fidesops.schemas.api import BulkUpdateFailed\n'), (374, 'fidesops.schemas.api.BulkUpdateFailed', 'BulkUpdateFailed', ([], {}), False, 'from fidesops.schemas.api import BulkUpdateFailed\n')]