from __future__ import annotations

import asyncio
import logging
from collections import defaultdict
from datetime import datetime, timezone
from itertools import islice
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple

from sqlmodel import Session, create_engine

from fmatch.shared.settings import get_settings

from ..db import AsyncSessionLocal
from ..models import Job, JobStatus
from ..services.crm_append_sessions import session_store
from ..services.salesforce_gateway import SalesforceGateway
from ..worker import (
    ProgressPhase,
    _job_pk,
    _set_job_meta,
    create_redis_connection,
    emit_progress,
)

log = logging.getLogger(__name__)

_settings = get_settings()
_DATABASE_URL = _settings.DATABASE_URL


def _run_async(func, *args, **kwargs):
    """Run an async coroutine in a synchronous RQ worker context."""
    try:
        return asyncio.run(func(*args, **kwargs))
    except RuntimeError as exc:
        # Fallback when an event loop is already running (tests)
        if "event loop" in str(exc).lower():
            loop = asyncio.new_event_loop()
            try:
                asyncio.set_event_loop(loop)
                return loop.run_until_complete(func(*args, **kwargs))
            finally:
                asyncio.set_event_loop(None)
                loop.close()
        raise


def _utc_now() -> datetime:
    return datetime.now(timezone.utc)


def run_crm_append_job(job_id: str) -> None:
    """
    Execute the CRM Append workflow.

    Flow overview:
      1. Load the queued job + session snapshot
      2. Detect key columns (company name, domain, optional SFDC Id)
      3. Query Salesforce for existing records (Id / Website / Name)
      4. Link existing accounts or queue creations
      5. Create missing accounts and persist summary
    """
    redis_conn = create_redis_connection()
    engine = create_engine(_DATABASE_URL)

    emit_progress(
        redis_conn,
        job_id,
        ProgressPhase.INITIALIZING,
        5,
        "Starting CRM append workflow",
    )
    _set_job_meta(
        redis_conn,
        job_id,
        status="processing",
        progress=5,
        message="Initializing CRM append",
    )

    try:
        with Session(engine) as db:
            job: Optional[Job] = db.get(Job, _job_pk(job_id))
            if not job:
                emit_progress(
                    redis_conn, job_id, ProgressPhase.FAILED, 100, "Job not found"
                )
                _set_job_meta(
                    redis_conn,
                    job_id,
                    status="failed",
                    progress=100,
                    message="Job not found",
                )
                log.error("CRM append job %s not found", job_id)
                return

            job.status = JobStatus.PROCESSING
            job.updated_at = _utc_now()
            db.add(job)
            db.commit()

            config = job.config if isinstance(job.config, Dict) else {}
            session_token = config.get("session_token")
            account_id = config.get("account_id") or job.account_id
            user_id = config.get("user_id") or job.user_id
            options = config.get("options", {})

            if not session_token:
                raise ValueError("No session_token in job config")

        _run_async(
            session_store.update_session,
            session_token,
            {
                "job": {
                    "id": job_id,
                    "status": "processing",
                    "started_at": _utc_now().isoformat(),
                }
            },
        )

        emit_progress(
            redis_conn,
            job_id,
            ProgressPhase.PREPROCESSING,
            15,
            "Loading session snapshot",
        )
        _set_job_meta(
            redis_conn,
            job_id,
            status="processing",
            progress=15,
            message="Loading snapshot",
        )

        session = _run_async(session_store.get_session, session_token)
        if not session:
            raise ValueError(f"Session {session_token} not found or expired")

        headers = session.get("sheet", {}).get("headers", [])
        rows = session.get("snapshot", {}).get("rows", [])
        total_rows = len(rows)

        if total_rows == 0:
            raise ValueError("No rows to process in session snapshot")

        name_col, domain_col, sfid_col = _detect_columns(headers)
        log.info(
            "CRM append job %s: total rows=%d, name_col=%s, domain_col=%s, sfid_col=%s",
            job_id,
            total_rows,
            name_col,
            domain_col,
            sfid_col,
        )

        payload = _run_async(
            _execute_crm_append_async,
            job_id,
            str(account_id),
            str(user_id) if user_id else None,
            headers,
            rows,
            name_col,
            domain_col,
            sfid_col,
            redis_conn,
            options,
        )

        summary = payload["summary"]
        results = payload["results"]

        with Session(engine) as db:
            job = db.get(Job, _job_pk(job_id))
            if job:
                job.results = {"summary": summary, "details": results}
                job.status = JobStatus.COMPLETED
                job.updated_at = _utc_now()
                db.add(job)
                db.commit()

        emit_progress(
            redis_conn,
            job_id,
            ProgressPhase.COMPLETED,
            100,
            f"CRM append complete: {summary['success']} successful, {summary['skipped']} skipped, {summary['failed']} failed",
        )
        _set_job_meta(
            redis_conn,
            job_id,
            status="completed",
            progress=100,
            message="CRM append completed",
            processed_rows=total_rows,
            result_rows=summary.get("success", 0),
            summary=summary,
        )

        _run_async(
            session_store.update_session,
            session_token,
            {
                "job": {
                    "id": job_id,
                    "status": "completed",
                    "completed_at": _utc_now().isoformat(),
                    "summary": summary,
                }
            },
        )

        log.info(
            "CRM append job %s completed: total=%d success=%d linked=%d created=%d skipped=%d failed=%d",
            job_id,
            summary.get("total_rows", total_rows),
            summary.get("success", 0),
            summary.get("linked", 0),
            summary.get("created", 0),
            summary.get("skipped", 0),
            summary.get("failed", 0),
        )

    except Exception as exc:
        log.exception("CRM append job %s failed: %s", job_id, exc)
        emit_progress(redis_conn, job_id, ProgressPhase.FAILED, 100, f"Failed: {exc}")
        _set_job_meta(
            redis_conn,
            job_id,
            status="failed",
            progress=100,
            message=str(exc),
            error=str(exc),
        )

        with Session(create_engine(_DATABASE_URL)) as db:
            job = db.get(Job, _job_pk(job_id))
            if job:
                job.status = JobStatus.FAILED
                job.error = str(exc)
                job.updated_at = _utc_now()
                db.add(job)
                db.commit()


def _detect_columns(headers: Sequence[str]) -> Tuple[int, Optional[int], Optional[int]]:
    """Return indexes for company name, domain, and optional Salesforce Id columns."""
    header_list = list(headers or [])
    lowered = [h.lower().strip() for h in header_list]

    def _find(candidates: Iterable[str]) -> Optional[int]:
        for candidate in candidates:
            cand = candidate.lower().strip()
            if cand in lowered:
                return lowered.index(cand)
        return None

    name_candidates = ["company", "company name", "account", "account name", "name"]
    domain_candidates = ["domain", "website", "company website", "site", "url"]
    sfid_candidates = [
        "sfdc_id",
        "salesforce_id",
        "accountid",
        "account id",
        "sfid",
        "sfdc id",
    ]

    name_col = _find(name_candidates)
    if name_col is None and header_list:
        name_col = 0

    domain_col = _find(domain_candidates)
    sfid_col = _find(sfid_candidates)

    if name_col is None:
        raise ValueError("Cannot detect a company/name column in headers")

    return name_col, domain_col, sfid_col


def _safe_cell(row: Sequence[Any], index: Optional[int]) -> Optional[str]:
    if index is None:
        return None
    if index < 0 or index >= len(row):
        return None
    value = row[index]
    if value is None:
        return None
    text = str(value).strip()
    return text or None


def _escape_soql_literal(value: str) -> str:
    escaped = value.replace("\\", "\\\\")
    escaped = escaped.replace("'", "\\'")
    return escaped


def _escape_like_literal(value: str) -> str:
    escaped = _escape_soql_literal(value)
    escaped = escaped.replace("%", "\\%").replace("_", "\\_")
    return escaped


def _chunked(seq: Sequence[str], size: int) -> Iterable[List[str]]:
    it = iter(seq)
    while True:
        chunk = list(islice(it, size))
        if not chunk:
            break
        yield chunk


def _normalize_key(value: Optional[str]) -> Optional[str]:
    if value is None:
        return None
    text = value.strip().lower()
    return text or None


def _clean_domain(raw: str) -> str:
    """Clean domain string: remove protocol, www, trailing slash."""
    domain = raw.lower().strip()
    domain = domain.replace("http://", "").replace("https://", "")
    if domain.startswith("www."):
        domain = domain[4:]
    domain = domain.split("/")[0]
    return domain


def _summarize_results(
    results: List[Dict[str, Any]], total_rows: int
) -> Dict[str, Any]:
    summary = defaultdict(int)
    created_at = _utc_now().isoformat()

    for entry in results:
        status = entry.get("status")
        if status == "linked":
            summary["success"] += 1
            summary["linked"] += 1
        elif status == "created":
            summary["success"] += 1
            summary["created"] += 1
        elif status == "skipped":
            summary["skipped"] += 1
        elif status == "failed":
            summary["failed"] += 1

    summary["total_rows"] = total_rows
    summary.setdefault("success", 0)
    summary.setdefault("linked", 0)
    summary.setdefault("created", 0)
    summary.setdefault("skipped", 0)
    summary.setdefault("failed", 0)
    summary["completed_at"] = created_at
    return dict(summary)


def _bulk_failure_reason(payload: Optional[Dict[str, Any]]) -> str:
    if not payload:
        return "Bulk create failed"
    errors = []
    for key in ("error", "errorMessage", "Error", "ErrorMessage"):
        if payload.get(key):
            errors.append(str(payload[key]))
    if payload.get("errors") and isinstance(payload["errors"], list):
        errors.extend(str(item) for item in payload["errors"] if item)
    if not errors and payload.get("message"):
        errors.append(str(payload["message"]))
    if errors:
        return "; ".join(errors)
    return "Bulk create failed"


async def _fetch_accounts_by_ids(
    gateway: SalesforceGateway, ids: Sequence[str]
) -> Dict[str, Dict[str, Any]]:
    records: Dict[str, Dict[str, Any]] = {}
    id_list = [value for value in ids if value]
    for chunk in _chunked(id_list, 100):
        literals = ", ".join(f"'{_escape_soql_literal(val)}'" for val in chunk)
        soql = f"SELECT Id, Name, Website FROM Account WHERE Id IN ({literals})"
        try:
            response = await gateway.soql(
                soql, admin_override=True, user="crm_append_worker"
            )
        except Exception as exc:
            log.warning("Failed to query Salesforce accounts by Id: %s", exc)
            continue

        data = response.get("records") if isinstance(response, dict) else response
        if not data:
            continue
        for record in data:
            sfid = record.get("Id")
            if sfid:
                records[str(sfid)] = record
    return records


async def _fetch_accounts_by_domains(
    gateway: SalesforceGateway, domains: Sequence[str]
) -> Dict[str, Dict[str, Any]]:
    records: Dict[str, Dict[str, Any]] = {}
    domain_list = [value for value in domains if value]
    for chunk in _chunked(domain_list, 25):
        conditions = [
            f"(Website LIKE '%{_escape_like_literal(val)}%')" for val in chunk
        ]
        if not conditions:
            continue
        soql = "SELECT Id, Name, Website FROM Account WHERE " + " OR ".join(conditions)
        try:
            response = await gateway.soql(
                soql, admin_override=True, user="crm_append_worker"
            )
        except Exception as exc:
            log.warning("Salesforce domain query failed: %s", exc)
            continue

        data = response.get("records") if isinstance(response, dict) else response
        if not data:
            continue
        for record in data:
            website = record.get("Website")
            if website:
                clean = _clean_domain(str(website))
                if clean:
                    records[clean] = record
    return records


async def _fetch_accounts_by_names(
    gateway: SalesforceGateway, names: Sequence[str]
) -> Dict[str, Dict[str, Any]]:
    records: Dict[str, Dict[str, Any]] = {}
    name_list = [value for value in names if value]
    for chunk in _chunked(name_list, 100):
        literals = ", ".join(f"'{_escape_soql_literal(val)}'" for val in chunk)
        soql = f"SELECT Id, Name, Website FROM Account WHERE Name IN ({literals})"
        try:
            response = await gateway.soql(
                soql, admin_override=True, user="crm_append_worker"
            )
        except Exception as exc:
            log.warning("Salesforce name query failed: %s", exc)
            continue

        data = response.get("records") if isinstance(response, dict) else response
        if not data:
            continue
        for record in data:
            name = record.get("Name")
            if name:
                key = _normalize_key(str(name))
                if key:
                    records[key] = record
    return records


async def _execute_crm_append_async(
    job_id: str,
    account_id: str,
    user_id: Optional[str],
    headers: Sequence[str],
    rows: List[List[Any]],
    name_col: int,
    domain_col: Optional[int],
    sfid_col: Optional[int],
    redis_conn,
    options: Dict[str, Any],
) -> Dict[str, Any]:
    total_rows = len(rows)
    emit_progress(
        redis_conn, job_id, ProgressPhase.PREPROCESSING, 25, "Connecting to Salesforce"
    )
    _set_job_meta(
        redis_conn,
        job_id,
        status="processing",
        progress=25,
        message="Connecting to Salesforce",
        processed_rows=0,
    )

    async with AsyncSessionLocal() as async_db:
        gateway = await SalesforceGateway.for_tenant(account_id, async_db)

        sfids = set()
        domains = set()
        names = set()

        for row in rows:
            sfid_val = _safe_cell(row, sfid_col)
            if sfid_val:
                sfids.add(sfid_val)
            domain_val = _safe_cell(row, domain_col)
            if domain_val:
                domains.add(_clean_domain(domain_val))
            name_val = _safe_cell(row, name_col)
            if name_val:
                names.add(name_val)

        emit_progress(
            redis_conn,
            job_id,
            ProgressPhase.MATCHING,
            35,
            "Querying existing Salesforce accounts",
        )
        _set_job_meta(
            redis_conn,
            job_id,
            status="processing",
            progress=35,
            message="Querying Salesforce accounts",
            processed_rows=0,
        )

        account_by_id = await _fetch_accounts_by_ids(gateway, list(sfids))
        account_by_domain = await _fetch_accounts_by_domains(gateway, list(domains))
        account_by_name = await _fetch_accounts_by_names(gateway, list(names))

        results: List[Dict[str, Any]] = []
        create_targets: List[Dict[str, Any]] = []

        seen_combos: set[Tuple[Optional[str], Optional[str]]] = set()

        emit_progress(
            redis_conn,
            job_id,
            ProgressPhase.MATCHING,
            50,
            f"Processing {total_rows} rows",
        )
        _set_job_meta(
            redis_conn,
            job_id,
            status="processing",
            progress=50,
            message="Matching rows",
            processed_rows=0,
        )

        for idx, row in enumerate(rows):
            company_name = _safe_cell(row, name_col)
            domain_val = _safe_cell(row, domain_col)
            cleaned_domain = _clean_domain(domain_val) if domain_val else None
            sfid_val = _safe_cell(row, sfid_col)

            normalized_key = (
                _normalize_key(company_name),
                _normalize_key(cleaned_domain),
            )

            entry: Dict[str, Any] = {
                "row_index": idx,
                "company_name": company_name,
                "domain": cleaned_domain,
                "sfdc_id": None,
                "action": None,
                "status": None,
            }

            try:
                if not company_name:
                    entry.update(
                        {
                            "status": "skipped",
                            "action": "skip",
                            "reason": "Missing company name",
                        }
                    )
                elif normalized_key in seen_combos:
                    entry.update(
                        {
                            "status": "skipped",
                            "action": "skip",
                            "reason": "Duplicate row in selection",
                        }
                    )
                else:
                    seen_combos.add(normalized_key)

                    matched_record = None
                    if sfid_val and sfid_val in account_by_id:
                        matched_record = account_by_id[sfid_val]
                    elif cleaned_domain and cleaned_domain in account_by_domain:
                        matched_record = account_by_domain[cleaned_domain]
                    elif company_name:
                        name_key = _normalize_key(company_name)
                        if name_key and name_key in account_by_name:
                            matched_record = account_by_name[name_key]

                    if matched_record:
                        entry.update(
                            {
                                "status": "linked",
                                "action": "link",
                                "sfdc_id": matched_record.get("Id"),
                                "sfdc_name": matched_record.get("Name"),
                            }
                        )
                    else:
                        entry.update(
                            {
                                "status": "pending",
                                "action": "create",
                                "sfdc_id": None,
                            }
                        )
                        record = {"Name": company_name}
                        if cleaned_domain:
                            record["Website"] = cleaned_domain

                        create_targets.append(
                            {
                                "result_index": len(results),
                                "record": record,
                            }
                        )
                results.append(entry)

            except Exception as row_exc:
                log.warning("Error processing CRM append row %d: %s", idx, row_exc)
                entry.update(
                    {
                        "status": "failed",
                        "action": "error",
                        "reason": str(row_exc),
                    }
                )
                results.append(entry)

            if total_rows and (idx + 1) % max(1, total_rows // 10) == 0:
                progress = 50 + int(((idx + 1) / total_rows) * 30)
                message = f"Processed {idx + 1} of {total_rows} rows"
                emit_progress(
                    redis_conn, job_id, ProgressPhase.MATCHING, progress, message
                )
                _set_job_meta(
                    redis_conn,
                    job_id,
                    status="processing",
                    progress=progress,
                    message=message,
                    processed_rows=idx + 1,
                )

        created_success = 0

        if create_targets:
            emit_progress(
                redis_conn,
                job_id,
                ProgressPhase.SAVING_RESULTS,
                85,
                f"Creating {len(create_targets)} Salesforce accounts",
            )
            _set_job_meta(
                redis_conn,
                job_id,
                status="processing",
                progress=85,
                message=f"Creating {len(create_targets)} Salesforce accounts",
                processed_rows=total_rows,
            )

            try:
                records_to_create = [item["record"] for item in create_targets]
                bulk_result = await gateway.bulk_create("Account", records_to_create)

                successful = bulk_result.get("successfulResults", []) or []
                failed = bulk_result.get("failedResults", []) or []
                unprocessed = bulk_result.get("unprocessedRecords", []) or []

                success_idx = 0
                failed_idx = 0
                unprocessed_idx = 0

                for target in create_targets:
                    result_entry = results[target["result_index"]]
                    if success_idx < len(successful):
                        success_payload = successful[success_idx]
                        success_idx += 1
                        sf_id = success_payload.get("sfId") or success_payload.get("id")
                        result_entry.update(
                            {
                                "status": "created",
                                "sfdc_id": sf_id,
                                "message": success_payload.get("message"),
                            }
                        )
                        created_success += 1
                    else:
                        failure_payload = None
                        if failed_idx < len(failed):
                            failure_payload = failed[failed_idx]
                            failed_idx += 1
                        elif unprocessed_idx < len(unprocessed):
                            failure_payload = unprocessed[unprocessed_idx]
                            unprocessed_idx += 1

                        result_entry.update(
                            {
                                "status": "failed",
                                "reason": _bulk_failure_reason(failure_payload),
                            }
                        )

            except Exception as bulk_exc:
                log.error("Bulk create failed: %s", bulk_exc)
                for target in create_targets:
                    result_entry = results[target["result_index"]]
                    if result_entry.get("status") == "pending":
                        result_entry.update(
                            {
                                "status": "failed",
                                "reason": f"Bulk create error: {bulk_exc}",
                            }
                        )

        summary = _summarize_results(results, total_rows)

        emit_progress(
            redis_conn,
            job_id,
            ProgressPhase.SAVING_RESULTS,
            95,
            "Finalizing CRM append results",
        )
        _set_job_meta(
            redis_conn,
            job_id,
            status="processing",
            progress=95,
            message="Finalizing results",
            processed_rows=total_rows,
            result_rows=summary.get("success", 0),
            summary=summary,
        )

        return {"results": results, "summary": summary}
