from dbt.api import APIObject

from dbt.node_types import NodeType
from dbt.utils import deep_merge


UNPARSED_BASE_CONTRACT = {
    'type': 'object',
    'additionalProperties': False,
    'properties': {
        'package_name': {
            'type': 'string',
        },
        # filesystem
        'root_path': {
            'type': 'string',
            'description': 'The absolute path to the project root',
        },
        'path': {
            'type': 'string',
            'description': (
                'Relative path to the source file from the project root. '
                'Usually the same as original_file_path, but in some cases '
                'dbt will generate a path.'),
        },
        'original_file_path': {
            'type': 'string',
            'description': (
                'Relative path to the originating file from the project root.'
            ),
        }
    },
    'required': ['package_name', 'root_path', 'path', 'original_file_path']
}

UNPARSED_HAS_SQL_CONTRACT = {
    'properties': {
        'raw_sql': {
            'type': 'string',
            'description': (
                'For nodes defined in SQL files, this is just the contents '
                'of that file. For schema tests, snapshots, etc. this is '
                'generated by dbt.'),
        },
        'index': {
            'type': 'integer',
        }
    },
    'required': ['raw_sql']
}

UNPARSED_MACRO_CONTRACT = deep_merge(
    UNPARSED_BASE_CONTRACT,
    UNPARSED_HAS_SQL_CONTRACT
)

UNPARSED_NODE_CONTRACT = deep_merge(
    UNPARSED_BASE_CONTRACT,
    UNPARSED_HAS_SQL_CONTRACT,
    {
        'properties': {
            'name': {
                'type': 'string',
                'description': (
                    'Name of this node. For models, this is used as the '
                    'identifier in the database.'),
                'minLength': 1,
            },
            'resource_type': {
                'enum': [
                    NodeType.Model,
                    NodeType.Test,
                    NodeType.Analysis,
                    NodeType.Operation,
                    NodeType.Seed,
                    # we need this if parse_node is going to handle snapshots.
                    NodeType.Snapshot,
                    NodeType.RPCCall,
                ]
            },
        },
        'required': ['resource_type', 'name']
    }
)


class UnparsedMacro(APIObject):
    SCHEMA = UNPARSED_MACRO_CONTRACT


class UnparsedNode(APIObject):
    SCHEMA = UNPARSED_NODE_CONTRACT


COLUMN_TEST_CONTRACT = {
    'type': 'object',
    'additionalProperties': False,
    'properties': {
        'name': {
            'type': 'string',
            'description': 'The name of the column this test is for',
        },
        'description': {
            'type': 'string',
            'description': 'The description of this test',
        },
        'tests': {
            'type': 'array',
            'items': {
                "anyOf": [
                    # 'not_null', 'unique', ...
                    {'type': 'string'},
                    # 'relationships: {...}', 'accepted_values: {...}'
                    {'type': 'object', 'additionalProperties': True}
                ],
            },
            'description': 'The list of tests to perform',
        },
    },
    'required': ['name'],
}


UNPARSED_COLUMN_DESCRIPTION_CONTRACT = {
    'properties': {
        'columns': {
            'type': 'array',
            'items': COLUMN_TEST_CONTRACT,
        },
    },
}


UNPARSED_NODE_DESCRIPTION_CONTRACT = {
    'properties': {
        'name': {
            'type': 'string',
            'description': (
                'The name of this node, which is the name of the model it'
                'refers to'
            ),
            'minLength': 1,
        },
        'description': {
            'type': 'string',
            'description': (
                'The raw string description of the node after parsing the yaml'
            ),
        },
        'tests': {
            'type': 'array',
            'items': {
                "anyOf": [
                    {'type': 'string'},
                    {'type': 'object', 'additionalProperties': True}
                ],
            },
        },
    },
    'required': ['name'],
}


UNPARSED_NODE_UPDATE_CONTRACT = deep_merge(
    UNPARSED_NODE_DESCRIPTION_CONTRACT,
    UNPARSED_COLUMN_DESCRIPTION_CONTRACT,
    {
        'type': 'object',
        'additionalProperties': False,
        'description': (
            'A collection of the unparsed node updates, as provided in the '
            '"models" section of schema.yml'
        ),
    }
)


class UnparsedNodeUpdate(APIObject):
    """An unparsed node update is the blueprint for tests to be added and nodes
    to be updated, referencing a certain node (specifically, a Model or
    Source).
    """
    SCHEMA = UNPARSED_NODE_UPDATE_CONTRACT


TIME_CONTRACT = {
    'type': 'object',
    'additionalProperties': False,
    'properties': {
        'count': {
            'type': 'integer',
        },
        'period': {
            'enum': ['minute', 'hour', 'day'],
        },
    },
    'required': ['count', 'period'],
}


_FRESHNESS_CONTRACT = {
    'type': 'object',
    'additionalProperties': False,
    'properties': {
        'warn_after': {
            'anyOf': [
                {'type': 'null'},
                TIME_CONTRACT,
            ]
        },
        'error_after': {
            'anyOf': [
                {'type': 'null'},
                TIME_CONTRACT,

            ]
        },
    },
}


_QUOTING_CONTRACT = {
    'type': 'object',
    'additionalProperties': False,
    'properties': {
        'database': {'type': 'boolean'},
        'schema': {'type': 'boolean'},
        'identifier': {'type': 'boolean'},
    },
}


QUOTING_CONTRACT = {
    'properties': {
        'quoting': {
            'anyOf': [
                {'type': 'null'},
                _QUOTING_CONTRACT,
            ],
        },
    },
}


FRESHNESS_CONTRACT = {
    'properties': {
        'loaded_at_field': {
            'type': ['null', 'string'],
            'description': 'The field to use as the "loaded at" timestamp',
        },
        'freshness': {
            'anyOf': [
                {'type': 'null'},
                _FRESHNESS_CONTRACT,
            ],
        },
    },
}


UNPARSED_SOURCE_TABLE_DEFINITION_CONTRACT = deep_merge(
    UNPARSED_NODE_DESCRIPTION_CONTRACT,
    UNPARSED_COLUMN_DESCRIPTION_CONTRACT,
    FRESHNESS_CONTRACT,
    QUOTING_CONTRACT,
    {
        'description': (
            'A source table definition, as provided in the "tables" '
            'subsection of the "sources" section of schema.yml'
        ),
        'properties': {
            'identifier': {
                'type': 'string',
                'description': 'The identifier for the source table',
                'minLength': 1,
            },
        },
    }
)


UNPARSED_SOURCE_DEFINITION_CONTRACT = deep_merge(
    FRESHNESS_CONTRACT,
    QUOTING_CONTRACT,
    {
        'type': 'object',
        'additionalProperties': False,
        'description': (
            'A collection of the unparsed sources, as provided in the '
            '"sources" section of schema.yml'
        ),
        'properties': {
            'name': {
                'type': 'string',
                'description': 'The reference name of the source definition',
                'minLength': 1,
            },
            'loader': {
                'type': 'string',
                'description': 'The user-defined loader for this source',
                'minLength': 1,
            },
            'description': {
                'type': 'string',
                'description': 'The user-supplied description of the source',
            },
            'database': {
                'type': 'string',
                'description': 'The database name for the source table',
                'minLength': 1,
            },
            'schema': {
                'type': 'string',
                'description': 'The schema name for the source table',
                'minLength': 1,
            },
            'tables': {
                'type': 'array',
                'items': UNPARSED_SOURCE_TABLE_DEFINITION_CONTRACT,
                'description': 'The tables for this source',
                'minLength': 1,
            },
        },
        'required': ['name'],
    }
)


class UnparsedTableDefinition(APIObject):
    SCHEMA = UNPARSED_SOURCE_TABLE_DEFINITION_CONTRACT


class UnparsedSourceDefinition(APIObject):
    SCHEMA = UNPARSED_SOURCE_DEFINITION_CONTRACT

    @property
    def tables(self):
        return [UnparsedTableDefinition(**t) for t in self.get('tables', [])]


UNPARSED_DOCUMENTATION_FILE_CONTRACT = {
    'type': 'object',
    'additionalProperties': False,
    'properties': {
        'package_name': {
            'type': 'string',
        },
        # filesystem
        'root_path': {
            'type': 'string',
            'description': 'The absolute path to the project root',
        },
        'path': {
            'type': 'string',
            'description': (
                'Relative path to the source file from the project root. '
                'Usually the same as original_file_path, but in some cases '
                'dbt will generate a path.'),
        },
        'original_file_path': {
            'type': 'string',
            'description': (
                'Relative path to the originating file from the project root.'
            ),
        },
        'file_contents': {
            'type': 'string',
            'description': (
                'The raw text provided in the documentation block, presumably '
                'markdown.'
            ),
        },
        # TODO: I would like to remove this, but some graph error handling
        # cares about it.
        'resource_type': {
            'enum': [
                NodeType.Documentation,
            ]
        },
    },
    'required': [
        'package_name', 'root_path', 'path', 'original_file_path',
        'file_contents', 'resource_type'
    ],
}


class UnparsedDocumentationFile(APIObject):
    SCHEMA = UNPARSED_DOCUMENTATION_FILE_CONTRACT
