<?php
namespace EasyChat\Library\Crm;

use EasyChat\Library\Http\ExternalHttpClient;
use EasyChat\Library\Http\PublicUrlGuard;

final class AmoCrmProvider implements CrmProviderInterface {
    private const MAX_RESPONSE_BYTES = 1048576;
    private $registry;
    private $http;
    private $guard;

    public function __construct($registry, ?ExternalHttpClient $http = null, ?PublicUrlGuard $guard = null) {
        $this->registry = $registry;
        $this->http = $http ?: new ExternalHttpClient($registry);
        $this->guard = $guard ?: new PublicUrlGuard();
    }

    public function code(): string { return 'amocrm'; }

    public function capabilities(): array {
        return array('entity_modes'=>array('deal'),'pipelines'=>true,'prepare_fields'=>true,'auth_type'=>'long_lived_token');
    }

    public function testConnection(array $connection): array {
        $account = $this->call($connection, 'GET', '/api/v4/account', null);
        if (empty($account['success'])) return $account;
        $pipelines = $this->pipelines($connection);
        if (empty($pipelines['success'])) return $pipelines;
        return array(
            'success'=>true,
            'portal'=>AmoCrmDomain::normalize((string)($connection['base_domain'] ?? '')),
            'account_id'=>(int)($account['result']['id'] ?? 0),
            'account_name'=>(string)($account['result']['name'] ?? ''),
            'pipelines'=>$pipelines['pipelines'],
        );
    }

    public function pipelines(array $connection): array {
        $response = $this->call($connection, 'GET', '/api/v4/leads/pipelines', null);
        if (empty($response['success'])) return $response;
        $items = (array)($response['result']['_embedded']['pipelines'] ?? array());
        $result = array();
        foreach ($items as $pipeline) {
            if (!empty($pipeline['is_archive'])) continue;
            $statuses = array();
            foreach ((array)($pipeline['_embedded']['statuses'] ?? array()) as $status) {
                $id = (int)($status['id'] ?? 0);
                if ($id <= 0 || in_array($id, array(142, 143), true) || (int)($status['type'] ?? 0) === 1) continue;
                $statuses[] = array('id'=>$id,'name'=>(string)($status['name'] ?? ''),'color'=>(string)($status['color'] ?? ''));
            }
            if ($statuses) $result[] = array('id'=>(int)$pipeline['id'],'name'=>(string)$pipeline['name'],'statuses'=>$statuses);
        }
        return array('success'=>true,'pipelines'=>$result);
    }

    public function prepareFields(array $connection): array {
        $list = $this->call($connection, 'GET', '/api/v4/leads/custom_fields?limit=250', null);
        if (empty($list['success'])) return $list;
        $required = array(
            'origin_field_id'=>'EasyChat ID',
            'dialog_field_id'=>'Диалог EasyChat',
            'source_field_id'=>'Канал EasyChat',
        );
        $resolved = array();
        foreach ((array)($list['result']['_embedded']['custom_fields'] ?? array()) as $field) {
            foreach ($required as $key => $name) {
                if ((string)($field['name'] ?? '') === $name && (string)($field['type'] ?? '') === 'text') $resolved[$key] = (int)$field['id'];
            }
        }
        $missing = array();
        $missing_keys = array();
        foreach ($required as $key => $name) {
            if (!empty($resolved[$key])) continue;
            $missing[] = array('name'=>$name,'type'=>'text','is_api_only'=>false);
            $missing_keys[] = $key;
        }
        if ($missing) {
            $created = $this->call($connection, 'POST', '/api/v4/leads/custom_fields', $missing);
            if (empty($created['success'])) return $created;
            $rows = array_values((array)($created['result']['_embedded']['custom_fields'] ?? array()));
            foreach ($missing_keys as $index => $key) if (!empty($rows[$index]['id'])) $resolved[$key] = (int)$rows[$index]['id'];
        }
        foreach (array_keys($required) as $key) if (empty($resolved[$key])) return array('success'=>false,'error'=>'amocrm_fields_incomplete','retryable'=>false);
        return array('success'=>true,'field_mapping'=>$resolved);
    }

    public function syncConversation(array $connection, array $conversation): array {
        $existing_id = max(0, (int)($conversation['external_id'] ?? 0));
        $contact_id = max(0, (int)($conversation['external_contact_id'] ?? 0));
        if ($existing_id <= 0) {
            $recovered = $this->findLeadByOrigin($connection, (string)($conversation['origin_key'] ?? ''));
            if (empty($recovered['success'])) return $recovered;
            $existing_id = (int)($recovered['external_id'] ?? 0);
            if ($existing_id <= 0) {
                $contact = $this->ensureContact($connection, $conversation);
                if (empty($contact['success'])) return $contact;
                $contact_id = (int)($contact['contact_id'] ?? 0);
                $created = $this->createLead($connection, $conversation, $contact_id);
                if (empty($created['success'])) return $created;
                $existing_id = (int)$created['external_id'];
            }
        }
        $note = $this->appendNote($connection, $existing_id, $conversation);
        if (empty($note['success'])) return $note;
        $domain = AmoCrmDomain::normalize((string)($connection['base_domain'] ?? ''));
        return array(
            'success'=>true,'external_id'=>(string)$existing_id,'external_contact_id'=>(string)$contact_id,
            'external_operation_id'=>(string)($note['external_operation_id'] ?? ''),'entity_type'=>'deal',
            'external_url'=>'https://' . $domain . '/leads/detail/' . $existing_id,
        );
    }

    private function createLead(array $connection, array $conversation, int $contact_id): array {
        $mapping = (array)($connection['field_mapping'] ?? array());
        foreach (array('origin_field_id','dialog_field_id','source_field_id') as $key) {
            if (empty($mapping[$key])) return array('success'=>false,'error'=>'amocrm_fields_not_prepared','retryable'=>false);
        }
        $lead = array(
            'name'=>mb_substr((string)($conversation['title'] ?? 'Обращение из EasyChat'), 0, 255),
            'pipeline_id'=>(int)($connection['pipeline_id'] ?? 0),
            'status_id'=>(int)($connection['status_id'] ?? 0),
            'created_by'=>0,
            'custom_fields_values'=>array(
                $this->fieldValue((int)$mapping['origin_field_id'], (string)($conversation['origin_key'] ?? '')),
                $this->fieldValue((int)$mapping['dialog_field_id'], (string)($conversation['chat_url'] ?? '')),
                $this->fieldValue((int)$mapping['source_field_id'], (string)($conversation['source_label'] ?? '')),
            ),
            'tags_to_add'=>array(array('name'=>'EasyChat')),
            'request_id'=>'easychat-conversation',
        );
        if (!empty($connection['responsible_id'])) $lead['responsible_user_id'] = (int)$connection['responsible_id'];
        if (!empty($conversation['source_label'])) $lead['tags_to_add'][] = array('name'=>mb_substr((string)$conversation['source_label'], 0, 100));
        if ($contact_id > 0) $lead['_embedded'] = array('contacts'=>array(array('id'=>$contact_id,'is_main'=>true)));
        $response = $this->call($connection, 'POST', '/api/v4/leads', array($lead));
        if (empty($response['success'])) return $response;
        $id = (int)($response['result']['_embedded']['leads'][0]['id'] ?? 0);
        return $id > 0 ? array('success'=>true,'external_id'=>$id) : array('success'=>false,'error'=>'amocrm_invalid_lead_response','retryable'=>false);
    }

    private function appendNote(array $connection, int $lead_id, array $conversation): array {
        $marker = (string)($conversation['message_marker'] ?? '');
        if (($conversation['delivery_state'] ?? '') === 'uncertain' && $marker !== '') {
            $notes = $this->call($connection, 'GET', '/api/v4/leads/' . $lead_id . '/notes?limit=250&order[id]=desc', null);
            if (empty($notes['success'])) return $notes;
            foreach ((array)($notes['result']['_embedded']['notes'] ?? array()) as $note) {
                if (strpos((string)($note['params']['text'] ?? ''), $marker) !== false) {
                    return array('success'=>true,'external_operation_id'=>(string)($note['id'] ?? ''));
                }
            }
        }
        $text = trim((string)($conversation['summary'] ?? ''));
        if ($text === '') $text = 'Новое сообщение доступно в EasyChat.';
        $prefix = empty($conversation['external_id']) ? 'Новое обращение' : 'Новое входящее сообщение';
        $note_text = $prefix . ' · ' . (string)($conversation['source_label'] ?? 'EasyChat')
            . "\n\n" . (string)($conversation['customer_name'] ?? 'Клиент') . ":\n" . mb_substr($text, 0, 4000)
            . "\n\nОткрыть диалог в EasyChat:\n" . (string)($conversation['chat_url'] ?? '')
            . ($marker !== '' ? "\n\n" . $marker : '');
        $response = $this->call($connection, 'POST', '/api/v4/leads/' . $lead_id . '/notes', array(array(
            'note_type'=>'common','params'=>array('text'=>$note_text),'request_id'=>'easychat-message',
            'is_need_to_trigger_digital_pipeline'=>!empty($connection['trigger_digital_pipeline']),
        )));
        if (empty($response['success'])) return $response;
        return array('success'=>true,'external_operation_id'=>(string)($response['result']['_embedded']['notes'][0]['id'] ?? ''));
    }

    private function findLeadByOrigin(array $connection, string $origin): array {
        $field_id = (int)($connection['field_mapping']['origin_field_id'] ?? 0);
        if ($origin === '' || $field_id <= 0) return array('success'=>true,'external_id'=>0);
        $response = $this->call($connection, 'GET', '/api/v4/leads?limit=50&query=' . rawurlencode($origin), null);
        if (empty($response['success'])) return $response;
        $matches = array();
        foreach ((array)($response['result']['_embedded']['leads'] ?? array()) as $lead) {
            if ($this->customFieldContains((array)($lead['custom_fields_values'] ?? array()), $field_id, $origin)) $matches[] = (int)$lead['id'];
        }
        if (count($matches) > 1) return array('success'=>false,'error'=>'amocrm_origin_ambiguous','retryable'=>false);
        return array('success'=>true,'external_id'=>$matches ? $matches[0] : 0);
    }

    private function ensureContact(array $connection, array $conversation): array {
        $phone = trim((string)($conversation['phone'] ?? ''));
        $email = trim((string)($conversation['email'] ?? ''));
         
         
        if ($phone === '' && $email === '') return array('success'=>true,'contact_id'=>0);
        $matches = array();
        foreach (array($phone, $email) as $query) {
            if ($query === '') continue;
            $response = $this->call($connection, 'GET', '/api/v4/contacts?limit=50&query=' . rawurlencode($query), null);
            if (empty($response['success'])) return $response;
            foreach ((array)($response['result']['_embedded']['contacts'] ?? array()) as $contact) {
                if ($this->contactMatches($contact, $phone, $email)) $matches[(int)$contact['id']] = true;
            }
        }
        if (count($matches) === 1) return array('success'=>true,'contact_id'=>(int)array_key_first($matches));
        $fields = array();
        if ($phone !== '') $fields[] = array('field_code'=>'PHONE','values'=>array(array('value'=>$phone,'enum_code'=>'WORK')));
        if ($email !== '') $fields[] = array('field_code'=>'EMAIL','values'=>array(array('value'=>$email,'enum_code'=>'WORK')));
        $contact = array('name'=>mb_substr((string)($conversation['customer_name'] ?? 'Клиент EasyChat'), 0, 255),'created_by'=>0,'custom_fields_values'=>$fields,'request_id'=>'easychat-contact');
        if (count($matches) > 1) $contact['tags_to_add'] = array(array('name'=>'EasyChat: требуется сверка'));
        $response = $this->call($connection, 'POST', '/api/v4/contacts', array($contact));
        if (empty($response['success'])) return $response;
        $id = (int)($response['result']['_embedded']['contacts'][0]['id'] ?? 0);
        return $id > 0 ? array('success'=>true,'contact_id'=>$id) : array('success'=>false,'error'=>'amocrm_invalid_contact_response','retryable'=>false);
    }

    private function contactMatches(array $contact, string $phone, string $email): bool {
        foreach ((array)($contact['custom_fields_values'] ?? array()) as $field) {
            $code = strtoupper((string)($field['field_code'] ?? ''));
            foreach ((array)($field['values'] ?? array()) as $value) {
                $actual = (string)($value['value'] ?? '');
                if ($code === 'PHONE' && $phone !== '' && $this->normalizePhone($actual) === $this->normalizePhone($phone)) return true;
                if ($code === 'EMAIL' && $email !== '' && strtolower(trim($actual)) === strtolower(trim($email))) return true;
            }
        }
        return false;
    }

    private function normalizePhone(string $value): string { return preg_replace('/[^0-9]+/', '', $value); }
    private function fieldValue(int $id, string $value): array { return array('field_id'=>$id,'values'=>array(array('value'=>$value))); }
    private function customFieldContains(array $fields, int $id, string $expected): bool {
        foreach ($fields as $field) if ((int)($field['field_id'] ?? 0) === $id) foreach ((array)($field['values'] ?? array()) as $value) if ((string)($value['value'] ?? '') === $expected) return true;
        return false;
    }

    private function call(array $connection, string $method, string $path, ?array $body): array {
        $domain = AmoCrmDomain::normalize((string)($connection['base_domain'] ?? ''));
        $token = trim((string)($connection['access_token'] ?? ''));
        if ($domain === '' || $token === '' || !preg_match('#^/api/v4/[A-Za-z0-9_/?=&%\[\].-]+$#', $path)) return array('success'=>false,'error'=>'amocrm_credentials_invalid','retryable'=>false);
        $url = 'https://' . $domain . $path;
        $prepared = $this->guard->prepare($url, array('amocrm.ru','amocrm.com','kommo.com'));
        if (empty($prepared['success'])) return array('success'=>false,'error'=>'crm_remote_host_denied','retryable'=>false);
        if (!$this->throttle((int)($connection['connection_id'] ?? 0))) return array('success'=>false,'error'=>'amocrm_rate_limit_busy','retryable'=>true);
        $buffer = ''; $overflow = false;
        $request = array(
            'headers'=>array('Accept: application/json','Content-Type: application/json','Authorization: Bearer ' . $token),
            'connect_timeout'=>4,'timeout'=>15,'follow_location'=>false,'trust_profile'=>'amocrm',
            'curl_options'=>(array)$prepared['curl_options'],
            'write_function'=>function ($ch, string $chunk) use (&$buffer, &$overflow): int { if (strlen($buffer)+strlen($chunk)>self::MAX_RESPONSE_BYTES) { $overflow=true; return 0; } $buffer.=$chunk; return strlen($chunk); },
        );
        if ($body !== null) $request['body'] = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        $result = $this->http->request($method, $url, $request);
        $http = (int)($result['http_code'] ?? 0);
        $json = !$overflow && $buffer !== '' ? json_decode($buffer, true) : array();
        $json = is_array($json) ? $json : array();
        if (!$overflow && !empty($result['transport_ok']) && $http >= 200 && $http < 300) return array('success'=>true,'result'=>$json,'http_status'=>$http);
        $retryable = $http === 0 || in_array($http, array(408,425,429), true) || $http >= 500;
        $code = preg_replace('/[^a-z0-9_]/i', '', (string)($json['title'] ?? $json['detail'] ?? ''));
        return array('success'=>false,'error'=>$overflow?'crm_response_too_large':($code?:'amocrm_http_' . $http),'retryable'=>$retryable,'outcome_unknown'=>$body !== null && $retryable,'http_status'=>$http);
    }

    private function throttle(int $connection_id): bool {
        if ($connection_id <= 0 || !$this->registry) return true;
        $db = $this->registry->get('db');
        $prefix = easychat_db_prefix();
        $lock_name = 'ec_amo_rate_' . substr(hash('sha256', $prefix . ':' . $connection_id), 0, 40);
        $lock = $db->query("SELECT GET_LOCK('" . $db->escape($lock_name) . "', 2) AS `locked`");
        if ((int)($lock->row['locked'] ?? 0) !== 1) return false;
        try {
            $now = (int)floor(microtime(true) * 1000);
            $query = $db->query("SELECT `next_allowed_at_ms` FROM `{$prefix}crm_rate_limits` WHERE `connection_id`={$connection_id} LIMIT 1");
            $next = (int)($query->row['next_allowed_at_ms'] ?? 0);
            if ($next > $now) usleep(min(1000000, ($next - $now) * 1000));
            $scheduled = max((int)floor(microtime(true) * 1000), $next) + 200;
            $db->query("INSERT INTO `{$prefix}crm_rate_limits` (`connection_id`,`next_allowed_at_ms`,`date_modified`) VALUES ({$connection_id},{$scheduled},NOW()) ON DUPLICATE KEY UPDATE `next_allowed_at_ms`=VALUES(`next_allowed_at_ms`),`date_modified`=NOW()");
            return true;
        } finally {
            $db->query("SELECT RELEASE_LOCK('" . $db->escape($lock_name) . "')");
        }
    }
}
