if (select top 1 0 from sys.databases where name = 'AnP') is null create database AnP collate Latin1_General_CI_AS
go
use AnP

if object_id(N'dbo.common_tables_delete', N'P') is not null drop procedure dbo.common_tables_delete
go
create procedure dbo.common_tables_delete as begin 

    set nocount on

    if object_id(N'dbo.fk_sessions_user', N'F') is not null 
        alter table dbo.[Sessions] drop constraint fk_sessions_user

    -- Level 3.
    if object_id(N'dbo.Logs', N'U') is not null drop table dbo.Logs
    if object_id(N'dbo.Exceptions', N'U') is not null drop table dbo.Exceptions
    if object_id(N'dbo.Terminals', N'U') is not null drop table dbo.Terminals
    if object_id(N'dbo.SessionsIps', N'U') is not null drop table dbo.SessionsIps

    -- Level 2.
    if object_id(N'dbo.[Sessions]', N'U') is not null drop table dbo.[Sessions]

    -- Level 1.
    if object_id(N'dbo.[Procedures]', N'U') is not null drop table dbo.[Procedures]
    if object_id(N'dbo.Users', N'U') is not null drop table dbo.Users

    -- Level 0.
    if object_id(N'dbo.Ips', N'U') is not null drop table dbo.Ips
    if object_id(N'dbo.[Databases]', N'U') is not null drop table dbo.[Databases]
    if object_id(N'dbo.Hashes', N'U') is not null drop table dbo.Hashes
    if object_id(N'dbo.[Messages]', N'U') is not null drop table dbo.[Messages]
    if object_id(N'dbo.Errors', N'U') is not null drop table dbo.Errors

    -- Level historical.
    if object_id(N'dbo.UsersData', N'U') is not null drop table dbo.UsersData

end
go

if object_id(N'dbo.common_tables_create', N'P') is not null drop procedure dbo.common_tables_create
go
create procedure dbo.common_tables_create as begin 

    set nocount on

    -- Level historical.
    if object_id(N'dbo.UsersData', N'U') is null create table dbo.UsersData(
        id integer not null identity(1, 1), 
        entity integer not null,
        [procedure] integer not null,
        [session] integer not null,
        [hash] integer, 
        nick varchar(32) collate database_default,
        [password] binary(64), 
        accessible bit,
        date_in datetime not null,
        date_out datetime, 
        constraint pk_users_data primary key clustered (id)
    )

    -- Level 0.
    if object_id(N'dbo.[Databases]', N'U') is null create table dbo.[Databases](
        id integer not null identity(1, 1), 
        [name] varchar(64) collate database_default not null, 
        date_in datetime not null constraint df_databases_date_in default getdate(),
        date_out datetime, 
        constraint pk_databases primary key clustered (id), 
        constraint uk_databases_name unique nonclustered ([name] asc) with (fillfactor = 90),
        constraint ck_databases_name check (
            [name] != '' and 
            [name] like '[a-z_]%' and 
            [name] not like '%[^a-z0-9_]%'
        )
    )

    if object_id(N'dbo.[Messages]', N'U') is null create table dbo.[Messages](
        id integer not null identity(1, 1), 
        [message] varchar(max) collate database_default not null,
        date_in datetime not null constraint df_messages_date_in default getdate(),
        date_out datetime, 
        constraint pk_messages primary key clustered (id)
    )

    if object_id(N'dbo.Hashes', N'U') is null create table dbo.Hashes(
        id integer not null identity(1, 1), 
        [name] varchar(16) collate database_default not null,
        date_in datetime not null constraint df_hashes_date_in default getdate(),
        date_out datetime, 
        constraint pk_hashes primary key clustered (id), 
        constraint uk_hashes_name unique nonclustered ([name] asc) with (fillfactor = 90), 
        constraint ck_hashes_name check ([name] != '')
    )

    if object_id(N'dbo.Errors', N'U') is null create table dbo.Errors(
        id integer not null identity(1, 1), 
        [error] varchar(512) collate database_default not null,
        date_in datetime not null constraint df_errors_date_in default getdate(),
        date_out datetime, 
        constraint pk_errors primary key clustered (id), 
        constraint uk_errors_error unique nonclustered ([error] asc) with (fillfactor = 90)
    )

    create table dbo.Ips(
        id integer not null identity(1, 1), 
        [ip] varchar(45) collate database_default not null, -- 3x4+3=15 for IPv4; 8x4+7=39 for IPv6; 30+15 for Hibrid.
        date_in datetime not null constraint df_ips_date_in default getdate(),
        date_out datetime, 
        constraint pk_ips primary key clustered (id), 
        constraint uk_ips_ip unique nonclustered ([ip] asc) with (fillfactor = 90), 
        constraint ck_ips_ip check (len([ip]) > 1 and [ip] not like '%[^0-9a-f:.]%'), 
        index ix_ips_ip nonclustered ([ip] asc) with (fillfactor = 90)
    )

    -- Level 1.
    if object_id(N'dbo.[Procedures]', N'U') is null begin 
        create table dbo.[Procedures](
            id integer not null identity(1, 1), 
            [database] integer not null, 
            [name] varchar(64) collate database_default not null, 
            date_in datetime not null constraint df_procedures_date_in default getdate(),
            date_out datetime, 
            constraint pk_procedures primary key clustered (id), 
            constraint fk_procedures_database foreign key([database]) references dbo.[Databases](id), 
            constraint uk_procedures_name unique([database], [name]), 
            constraint ck_procedures_database check ([database] > 0), 
            constraint ck_procedures_name check (
                [name] != '' and 
                [name] like '[a-z_]%' and 
                [name] not like '%[^a-z0-9_]%'
            )
        )
        create nonclustered index ix_procedures_name on dbo.[Procedures]([name] asc) include ([database]) with (fillfactor = 90)
    end

    if object_id(N'dbo.Users', N'U') is null create table dbo.Users(
        id integer not null identity(1, 1), 
        [hash] integer not null constraint df_users_hash default 1, 
        nick varchar(32) collate database_default not null, 
        [password] binary(64), 
        accessible bit not null constraint df_users_accessible default 1,
        date_in datetime not null constraint df_users_date_in default getdate(),
        date_out datetime, 
        constraint pk_users_id primary key clustered (id), 
        constraint fk_users_hash foreign key([hash]) references dbo.Hashes(id), 
        constraint uk_users_nick unique nonclustered (nick asc) with (fillfactor = 90), 
        constraint ck_users_hash check ([hash] > 0),
        constraint ck_users_nick check (nick != ''), 
        index ix_users_hash nonclustered ([hash] asc) with (fillfactor = 90), 
        index ix_users_nick nonclustered (nick asc) with (fillfactor = 90)
    )

    -- level 2.
    if object_id(N'dbo.[Sessions]', N'U') is null create table dbo.[Sessions](
        id integer not null identity(1, 1), 
        [user] integer not null, 
        date_last datetime not null constraint df_sessions_date_last default getdate(),
        date_in datetime not null constraint df_sessions_date_in default getdate(),
        date_out datetime, 
        constraint pk_sessions primary key clustered (id), 
        constraint fk_sessions_user foreign key([user]) references dbo.Users(id),
        constraint ck_sessions_user check ([user] > 0), 
        index ix_sessions_user nonclustered ([user] asc) with (fillfactor = 90)
    )

    -- Level 3.
    if object_id(N'dbo.Logs', N'U') is null begin 
        create table dbo.Logs(
            id integer not null identity(1, 1), 
            [procedure] integer not null,
            [session] integer not null,
            [message] integer not null,
            error integer,
            parameters text,
            date_in datetime not null constraint df_logs_date_in default getdate(),
            date_out datetime, 
            constraint pk_logs primary key clustered (id), 
            constraint fk_logs_procedure foreign key([procedure]) references dbo.[Procedures](id), 
            constraint fk_logs_session foreign key([session]) references dbo.[Sessions](id),
            constraint fk_logs_message foreign key([message]) references dbo.[Messages](id), 
            constraint fk_logs_error foreign key(error) references dbo.Errors(id), 
            constraint ck_logs_procedure check ([procedure] > 0), 
            constraint ck_logs_session check ([session] > 0),
            constraint ck_logs_message check ([message] > 0),
            constraint ck_logs_error check (error is null or error > 0)
        )
        create nonclustered index ix_logs_procedure on dbo.Logs ([procedure] asc) include ([session], [message], error) with (fillfactor = 90)
        create nonclustered index ix_logs_session on dbo.Logs ([session] asc) include ([procedure], [message], error) with (fillfactor = 90)
        create nonclustered index ix_logs_error on dbo.Logs (error asc) include ([procedure], [session], [message]) with (fillfactor = 90)
        create nonclustered index ix_logs_message on dbo.Logs ([message] asc) include ([procedure], [session], error) with (fillfactor = 90)
    end

    if object_id(N'dbo.Exceptions', N'U') is null begin 
        create table dbo.Exceptions(
            id integer not null identity(1, 1), 
            [procedure] integer not null,
            [session] integer not null,
            [message] integer not null,
            exception integer not null, 
            parameters text,
            [status] varchar(16) collate database_default, 
            code integer, 
            date_in datetime not null constraint df_exceptions_date_in default getdate(),
            date_out datetime, 
            constraint pk_exceptions primary key clustered (id), 
            constraint fk_exceptions_procedure foreign key([procedure]) references dbo.[Procedures](id), 
            constraint fk_exceptions_session foreign key([session]) references dbo.[Sessions](id),
            constraint fk_exceptions_message foreign key([message]) references dbo.[Messages](id),
            constraint fk_exceptions_exception foreign key([exception]) references dbo.[Messages](id), 
            constraint ck_exceptions_procedure check ([procedure] > 0), 
            constraint ck_exceptions_session check ([session] > 0),
            constraint ck_exceptions_message check ([message] > 0),
            constraint ck_exceptions_exception check ([exception] > 0)
        )
        create nonclustered index ix_exceptions_procedure on dbo.Exceptions ([procedure] asc) include ([session], [message], exception) with (fillfactor = 90)
        create nonclustered index ix_exceptions_session on dbo.Exceptions ([session] asc) include ([procedure], [message], exception) with (fillfactor = 90)
        create nonclustered index ix_exceptions_exception on dbo.Exceptions (exception asc) include ([procedure], [session], [message]) with (fillfactor = 90)
        create nonclustered index ix_exceptions_message on dbo.Exceptions ([message] asc) include ([procedure], [session], exception) with (fillfactor = 90)
    end

    if object_id(N'dbo.Terminals', N'U') is null begin 
        create table dbo.Terminals(
            id integer not null identity(1, 1), 
            [procedure] integer not null,
            [session] integer not null,
            [message] varchar(max) collate database_default, 
            date_in datetime not null constraint df_terminals_date_in default getdate(),
            date_out datetime, 
            constraint pk_terminals_id primary key clustered (id), 
            constraint fk_terminals_procedure foreign key([procedure]) references dbo.[Procedures](id), 
            constraint fk_terminals_session foreign key([session]) references dbo.[Sessions](id), 
            constraint ck_terminals_procedure check ([procedure] > 0), 
            constraint ck_terminals_session check ([session] > 0)
        )
        create nonclustered index ix_terminals_procedure on dbo.Terminals ([procedure] asc) include ([session]) with (fillfactor = 90)
        create nonclustered index ix_terminals_session on dbo.Terminals ([session] asc) include ([procedure]) with (fillfactor = 90)
    end

    if object_id(N'dbo.SessionsIps', N'U') is null begin 
        create table dbo.SessionsIps(
            id integer not null identity(1, 1), 
            [session] integer not null, 
            [ip] integer not null, 
            date_in datetime not null constraint df_sessions_ips_date_in default getdate(),
            date_out datetime, 
            constraint pk_sessions_ips_id primary key clustered (id), 
            constraint fk_sessions_ips_session foreign key([session]) references dbo.[Sessions](id),
            constraint fk_sessions_ips_ip foreign key([ip]) references dbo.Ips(id), 
            constraint ck_sessions_ips_session check ([session] > 0), 
            constraint ck_sessions_ips_ip check ([ip] > 0)
        )
        create nonclustered index ix_sessions_ips_session on dbo.SessionsIps ([session] asc) include ([ip]) with (fillfactor = 90)
        create nonclustered index ix_sessions_ips_ip on dbo.SessionsIps ([ip] asc) include ([session]) with (fillfactor = 90)
    end

end
go

if object_id(N'dbo.common_tables_update', N'P') is not null drop procedure dbo.common_tables_update
go
create procedure dbo.common_tables_update as begin 

    set nocount on

end
go

execute dbo.common_tables_delete
go
execute dbo.common_tables_create
go
execute dbo.common_tables_update
go

if object_id(N'dbo.sql_set', N'P') is not null drop procedure dbo.sql_set
go
create procedure dbo.sql_set
    @database varchar(64),
    @procedure varchar(64),
    @session integer
as begin 

    declare @data binary(128) = (
        cast(substring(isnull(@database, ''), 1, 62) as binary(62)) + 
        cast(substring(isnull(@procedure, ''), 1, 62) as binary(62)) + 
        cast(isnull(@session, 0) as binary(4))
    )

    set nocount on 

    set context_info @data

end
go

if object_id(N'dbo.sql_get', N'P') is not null drop procedure dbo.sql_get
go
create procedure dbo.sql_get
    @database varchar(64) output,
    @procedure varchar(64) output,
    @session integer output
as begin 

    declare @context binary(128) = context_info()
    declare @null char(1) = char(0)

    set nocount on

    set @database = rtrim(replace(cast(substring(@context, 1, 62) as varchar(64)), @null, ''))
    set @procedure = rtrim(replace(cast(substring(@context, 63, 62) as varchar(64)), @null, ''))
    set @session = cast(substring(@context, 125, 4) as int)

end
go

if object_id(N'dbo.procedure_get', N'P') is not null drop procedure dbo.procedure_get
go
create procedure dbo.procedure_get
    @database varchar(64),
    @procedure varchar(64), 
    @id integer output
as begin 

    declare @database_id integer = (select top 1 id from dbo.[Databases] where 
        date_out is null and 
        [name] = @database
    )

    set nocount on

    if @database_id is null begin 
        insert into dbo.[Databases]([name]) values(@database)
        set @database_id = scope_identity()
    end

    set @id = (select top 1 id from dbo.[Procedures] where 
        date_out is null and 
        [database] = @database_id and 
        [name] = @procedure
    )

    if @id is null begin 
        insert into dbo.[Procedures]([database], [name]) values(@database_id, @procedure)
        set @id = scope_identity()
    end

end
go

if object_id(N'dbo.users_trigger_update', N'TR') is not null drop trigger dbo.users_trigger_update
go
create trigger dbo.users_trigger_update on dbo.Users after insert, update as begin 

    declare @session integer
    declare @database varchar(64)
    declare @procedure varchar(64)
    declare @procedure_id integer
    declare @id integer
    declare @hash integer
    declare @nick varchar(32)
    declare @password binary(64)
    declare @accessible bit
    declare @date_in datetime
    declare @date_out datetime
    declare users_cursor cursor local for (
        select id, [hash], nick, [password], accessible, date_in, date_out from inserted
    )

    set nocount on

    execute dbo.sql_get @database output, @procedure output, @session output
    execute dbo.procedure_get @database, @procedure, @procedure_id output

    open users_cursor
    while 1 = 1 begin
        fetch next from users_cursor into @id, @hash, @nick, @password, @accessible, @date_in, @date_out
        if @@fetch_status != 0 
            break
        insert into dbo.UsersData(entity, [procedure], [session], [hash], nick, [password], accessible, date_in, date_out) values 
            (@id, @procedure_id, @session, @hash, @nick, @password, @accessible, @date_in, @date_out)
    end
    close users_cursor
    deallocate users_cursor

end
go

if object_id(N'dbo.hash_create_name', N'FN') is not null drop function dbo.hash_create_name
go
create function dbo.hash_create_name(
    @method varchar(16)
) returns varchar(16) as begin 
    return (case 
        when @method = 'md5' then 'md5'
        when @method = 'sha1' then 'sha1'
        when @method in (
            'sha2_256', 'sha2-256', 'sha-256', 'sha256', 'sha_256'
        ) then 'sha2_256'
        when @method in (
            'sha2_512', 'sha2-512', 'sha-512', 'sha512', 'sha_512'
        ) then 'sha2_512'
    else null end)
end
go

if object_id(N'dbo.hash_create', N'FN') is not null drop function dbo.hash_create
go
create function dbo.hash_create(
    @method varchar(16), 
    @input varchar(max)
) returns binary(64) as begin 

    set @method = dbo.hash_create_name(@method)

    return (case 
        when @method in (
            'md5', 'sha1', 'sha2_256', 'sha2_512'
        ) then hashbytes(upper(@method), @input)
    else cast(@input as binary(64)) end)
end
go

if object_id(N'dbo.message_get', N'P') is not null drop procedure dbo.message_get
go
create procedure dbo.message_get
    @message varchar(max),
    @id integer output
as begin

    set nocount on

    set @id = (select top 1 id from dbo.[Messages] where date_out is null and [message] = @message)

    if @id is null begin 
        insert into dbo.[Messages]([message]) values(@message)
        set @id = scope_identity()
    end

end
go

if object_id(N'dbo.error_get', N'P') is not null drop procedure dbo.error_get
go
create procedure dbo.error_get
    @error varchar(512),
    @id integer output
as begin

    set nocount on

    set @id = (select top 1 id from dbo.Errors where date_out is null and [error] = @error)

    if @id is null begin 
        insert into dbo.Errors([error]) values(@error)
        set @id = scope_identity()
    end

end
go

if object_id(N'dbo.hash_get', N'P') is not null drop procedure dbo.hash_get
go
create procedure dbo.hash_get
    @name varchar(16),
    @id integer output
as begin

    set nocount on

    set @id = (select top 1 id from dbo.Hashes where date_out is null and [name] = @name)

    if @id is null begin 
        insert into dbo.Hashes([name]) values(@name)
        set @id = scope_identity()
    end

end
go

if object_id(N'dbo.logs_set', N'P') is not null drop procedure dbo.logs_set
go
create procedure dbo.logs_set 
    @session integer, 
    @database varchar(64), 
    @procedure varchar(64), 
    @message varchar(max), 
    @parameters varchar(max), 
    @error varchar(512) = '', 
    @level tinyint = 0
as begin 

    declare @procedure_id integer
    declare @error_id integer
    declare @message_id integer

    set nocount on

    execute dbo.procedure_get @database, @procedure, @procedure_id output
    execute dbo.message_get @message, @message_id output
    execute dbo.error_get @error, @error_id output

    insert into dbo.Logs([procedure], [session], [message], parameters, [error]) values
        (@procedure_id, @session, @message_id, @parameters, @error_id)
    
end
go

if object_id(N'dbo.logs_set', N'P') is not null drop procedure dbo.logs_set
go
create procedure dbo.logs_set 
    @session integer, 
    @database varchar(64), 
    @procedure varchar(64), 
    @message varchar(max), 
    @parameters varchar(max)
as begin 

    declare @exception varchar(max) = error_message()
	declare @status varchar(16) = error_state()
	declare @code integer = error_number()
	declare @exception_id integer
    declare @procedure_id integer
    declare @message_id integer

    set nocount on

    execute dbo.message_get @exception, @exception_id output
    execute dbo.procedure_get @database, @procedure, @procedure_id output
    execute dbo.message_get @message, @message_id output

    insert into dbo.Exceptions([procedure], [session], [message], exception, [parameters], [status], code) values
        (@procedure_id, @session, @message_id, @exception_id, @parameters, @status, @code)
    
end
go

if object_id(N'dbo.common_tables_fill', N'P') is not null drop procedure dbo.common_tables_fill
go
create procedure dbo.common_tables_fill as begin 

    declare @database varchar(64) = 'AnP'
    declare @procedure varchar(64) = 'common_tables_fill'
    declare @procedure_id integer
    declare @local_ip varchar(71) = '::1'
    declare @main_user varchar(32) = 'machine'
    declare @user varchar(32)
    declare @session_id integer = (
        select top 1 id from dbo.[Sessions] where date_out is null and [user] = (
            select top 1 id from dbo.Users where date_out is null and nick = @main_user
        )
    )
    declare @has_session bit = case when @session_id is null then 0 else 1 end
    declare @ip_id integer = (
        select top 1 id from dbo.Ips where date_out is null and ip = @local_ip
    )
    declare @hash_id integer
    declare @hash varchar(16)
    declare users_cursor cursor local for (
        select @main_user as [name] union all 
        select 'guest'
    )
    declare hashes_cursor cursor local for (
        select 'null' as [name] union all 
        select 'md5' union all 
        select 'sha1' union all 
        select 'sha2_256' union all 
        select 'sha2_512'
    )

    set nocount on

    if @has_session = 0 begin 
        if (select top 1 0 from sys.indexes where name = N'ix_sessions_user') is not null 
            drop index ix_sessions_user on dbo.[Sessions]
        if object_id(N'dbo.fk_sessions_user', N'F') is not null 
            alter table dbo.[Sessions] drop constraint fk_sessions_user
        alter table dbo.[Sessions] alter column [user] integer
        insert into dbo.[Sessions] default values
        set @session_id = scope_identity()
    end

    execute dbo.sql_set @database, @procedure, @session_id
    execute dbo.procedure_get @database, @procedure, @procedure_id output

    open hashes_cursor
    while 1 = 1 begin
        fetch next from hashes_cursor into @hash
        if @@fetch_status != 0 
            break
        if (select top 1 0 from dbo.Hashes where date_out is null and [name] = @hash) is null 
            insert into dbo.Hashes([name]) values(@hash)
    end
    close hashes_cursor
    deallocate hashes_cursor

    execute dbo.hash_get 'null', @hash_id output

    open users_cursor
    while 1 = 1 begin 
        fetch next from users_cursor into @user
        if @@fetch_status != 0 
            break
        if (select top 1 0 from dbo.Users where date_out is null and nick = @user) is null 
            insert into dbo.Users([hash], nick) values
                (@hash_id, @user)
    end
    close users_cursor
    deallocate users_cursor

    if @has_session = 0 begin 
        update dbo.[Sessions] set [user] = (
            select top 1 id from dbo.Users where date_out is null and nick = @main_user
        ) where id = @session_id
        alter table dbo.[Sessions] alter column [user] integer not null
        alter table dbo.[Sessions] add constraint fk_sessions_user foreign key([user]) references dbo.Users(id)
        create nonclustered index ix_sessions_user on dbo.[Sessions]([user] asc) with (fillfactor = 90)
    end

    if @ip_id is null begin 
        insert into dbo.Ips([ip]) values(@local_ip)
        set @ip_id = scope_identity()
    end
    if (select top 1 0 from dbo.SessionsIps where 
        date_out is null and 
        [session] = @session_id and 
        [ip] = @ip_id
    ) is null 
        insert into dbo.SessionsIps([session], [ip]) values(@session_id, @ip_id)

end
go

execute dbo.common_tables_fill
go

if object_id(N'dbo.where_set', N'FN') is not null drop function dbo.where_set
go
create function dbo.where_set(
    @old_where varchar(max), 
    @new_where varchar(max)
) returns varchar(max) as begin 

    set @new_where = ltrim(rtrim(isnull(@new_where, '')))
    set @old_where = ltrim(rtrim(isnull(@old_where, '')))

    return (case 
        when @new_where = '' then @old_where
        when @old_where = '' then @new_where 
    else '(' + @old_where + ') and (' + @new_where + ')' end)
end
go

if object_id(N'dbo.tables_list', N'P') is not null drop procedure dbo.tables_list
go
create procedure dbo.tables_list 
    @view varchar(max), 
    @page integer, 
    @items_per_page integer, 
    @where varchar(max), 
    @order_by varchar(max), 
    @has_errors bit, 
    @pages integer output, 
    @items integer output, 
    @results integer output
as begin 

    declare @where_string varchar(max) = (case 
        when @has_errors = 1 then ' where ' + dbo.where_set('0 = 1', @where) 
        when ltrim(rtrim(isnull(@where, ''))) = '' then ''
    else ' where ' + @where end)
    declare @query varchar(max) = N'select top 1 @subitems = count(1) from ' + @view + @where_string

    set nocount on

    execute sp_executesql @query, N'@subitems integer output', @subitems = @items output

    set @pages = ceiling(@items / cast(@items_per_page as float))
    set @query = (
        N'select * from ' + @view + @where_string + 
        (case when ltrim(rtrim(isnull(@order_by, ''))) = '' then '' else N' order by ' + @order_by end) + 
        N' offset ' + cast((((case 
            when @page < 1 then 1 
            when @page > @pages then @pages
        else @page end) - 1) * @items_per_page) as varchar) + N' rows fetch next ' + cast(@items_per_page as varchar) + N' rows only'
    )

    execute sp_executesql @query
    set @results = @@rowcount

end
go